Ethereum’s Fusaka upgrade cut gas fees roughly sixfold when it activated on December 3, 2025. That was good news for anyone sending a transaction. It was also good news for scammers, who used the cheaper fees to flood the network with address-poisoning attempts. Blockaid’s telemetry shows poisoning attempts jumping from 628,000 in November 2025 to 3.4 million in January 2026, a 5.5x increase in two months. Separately, Scam Sniffer’s 2025 annual report puts wallet-drainer phishing losses at $83.85 million for the year, spread across roughly 106,000 victims. Both numbers share a common root cause: wallets that still hold an open, unlimited token approval from months or years ago.
This tutorial walks through exactly how to find and shut down those approvals. You will build a small Node.js scanner that pulls approval history straight from a block explorer API, learn the manual process through Revoke.cash, and write a script that revokes both ERC-20 allowances and NFT operator permissions with ethers.js. By the end you will have a repeatable, 10-step process you can run on your own wallet (or a client’s) in under 30 minutes, plus a working project you can keep running on a schedule.
Why Revoking Token Approvals Matters in 2026
Every time you swap on a DEX, stake in a farm, or mint from a new contract, you typically sign an approval that lets that contract move tokens out of your wallet without asking again. Most interfaces default to requesting an unlimited allowance, because it saves the dapp from asking for permission on every single trade. That’s convenient right up until the contract you approved gets exploited, its owner keys get stolen, or the address itself was a scam from day one.
The scale of the problem has shifted rather than disappeared. Scam Sniffer’s numbers show drainer losses fell 83% year over year, from close to $494 million in 2024 down to $83.85 million in 2025, and the largest single theft dropped from $55.48 million to $6.5 million. That decline is real progress, driven partly by wallets shipping built-in warnings and partly by exchanges freezing stolen funds faster. But the attack surface hasn’t shrunk. Blockaid reports flagging more than 65.4 million address-poisoning transactions since January 2025, averaging over 160,000 a day, and roughly 316,000 of those actually succeeded in tricking a victim into sending funds to a copied address. Old, forgotten approvals are the quiet half of this story: they don’t need a victim to fall for a new trick, they just need to sit there until someone finds a way to abuse the contract that holds them.
MetaMask responded by shipping live address-poisoning detection inside its wallet UI in June 2026, built on the Blockaid dataset and a deepened Consensys Diligence security review process. That helps with new poisoning attempts, but it does nothing for the approval you granted a yield farm two years ago that nobody has audited since. Cleaning those up is a maintenance task, not a one-time fix, and this guide treats it that way.
The 2025-2026 Numbers Behind the Threat
It helps to see the trend lines side by side before you start clicking “revoke” on anything. Scam Sniffer’s year-end report shows drainer losses falling every quarter except the third, which lined up with Ethereum’s strongest price rally of the year and accounted for nearly 37% of the annual total on its own. That pattern matters for approvals specifically: drainers need an open allowance or a signed permit to move funds, so periods of high trading activity are also periods when people grant the most new approvals without thinking twice.
| Metric | 2024 | 2025 | Change |
|---|---|---|---|
| Total wallet-drainer phishing losses | ~$494M | $83.85M | -83% |
| Victims affected | ~332,000 | 106,000 | -68% |
| Largest single theft | $55.48M | $6.5M | -88.3% |
| Incidents over $1M | 30 | 11 | -63.3% |
Address poisoning tells a different, still-climbing story. Blockaid’s research found that the roughly one-year measurement window since January 2025 across Ethereum and BNB Chain turned up 65.4 million poisoning attempts, targeting more than 17 million unique victim addresses with roughly 50 million lookalike addresses generated to pull it off. Only roughly 316,000 of those attempts actually resulted in a confirmed transfer, but those confirmed hits still add up to more than $83.8 million in documented losses, a figure researchers expect is understated given how much goes unreported. Two incidents anchor just how bad a single mistake can get: a December 2025 victim who sent $50 million in USDT to a copied address, and a January 2026 victim who lost roughly 4,556 ETH (about $12.25 million at the time) the same way. Neither loss involved a stolen key. Both involved a wallet that looked, at a glance, like the right one.
Prerequisites: What You Need Before You Start
You can complete the manual half of this tutorial with just a browser and a wallet. The scripted half needs a small local development setup. Here’s the full list.
Wallet and Browser Requirements
- A self-custody wallet extension, such as MetaMask 13.x or a hardware wallet connected through a companion app
- The public address(es) you want to audit, on every chain where you’ve ever interacted with a dapp
- A small amount of native gas token (ETH, MATIC, BNB, and so on) on each chain, enough to cover a handful of revoke transactions
Developer Tools for the Scripted Method
- Node.js 20 LTS or newer (check with
node -v) - npm 10.x, bundled with Node.js
- ethers.js v6 for signing and sending transactions
- A free Etherscan API v2 key, which now covers all Etherscan-family explorers (Etherscan, BscScan, PolygonScan, Arbiscan, and more) through one endpoint and one key
- A code editor and basic comfort reading JavaScript
If you haven’t set up a self-custody wallet yet, work through our self-custody wallet setup guide first, since this tutorial assumes you already control your own keys rather than holding funds on an exchange.
How ERC-20 and NFT Approvals Actually Work
Two approval mechanisms cover most of what you’ll need to clean up. The first is the ERC-20 approve function, defined in the original ERC-20 standard. It takes a spender address and an amount, and it lets that spender call transferFrom on your tokens up to that amount, indefinitely, until you change it.
// ERC-20 approval interface (simplified)
interface IERC20 {
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
}
// A "max" approval, common in DEX and bridge UIs by default
// 2^256 - 1, effectively unlimited
const MAX_UINT256 = 2n ** 256n - 1n;
The second is setApprovalForAll, used by ERC-721 and ERC-1155 contracts. Instead of approving a spend amount, it hands an operator address blanket control over every token you own in that collection, including future mints. It’s a single boolean flag, so there’s no partial version of it, it’s either fully granted or fully off.
Both functions trace back to the original ERC-20 standard, which shipped in 2015 without any concept of a time limit or a spend cap that decays. That design choice is nobody’s fault in hindsight, token standards rarely anticipate a decade of billions of dollars in adversarial activity, but it does mean the burden of cleanup falls entirely on the wallet holder.
A third mechanism worth knowing about is Permit (EIP-2612) and Uniswap’s Permit2, which let you sign an off-chain message granting an allowance instead of sending an on-chain transaction. These don’t show up as a normal approval transaction in your history, which is exactly why block explorers built dedicated approval-checker tools rather than relying on users to scroll through raw transaction logs.
Step 1: Set Up Your Node.js Scanning Environment
Create a project folder and install the three packages this tutorial relies on: ethers for chain interaction, axios for API calls, and dotenv to keep your keys out of the source code.
mkdir approval-auditor && cd approval-auditor
npm init -y
npm install ethers@6 axios dotenv
mkdir scripts
You should see ethers install at version 6.x and a fresh package.json in the folder. If npm reports a peer dependency warning about Node engines, upgrade to Node 20 LTS before continuing, older runtimes occasionally mishandle ethers.js v6’s BigInt-based math.
Step 2: Configure API Keys and Environment Variables
Sign up for a free Etherscan account, then generate an API v2 key from the API Keys section of your dashboard. The v2 key works across every Etherscan-family chain, so you don’t need separate keys for BscScan or Arbiscan anymore. Create a .env file in your project root.
# .env
ETHERSCAN_API_KEY=your_api_v2_key_here
WALLET_ADDRESS=0xYourWalletAddressHere
RPC_URL_MAINNET=https://eth.llamarpc.com
RPC_URL_POLYGON=https://polygon-rpc.com
RPC_URL_ARBITRUM=https://arb1.arbitrum.io/rpc
# Only fill this in when you reach the signing step, never commit it
PRIVATE_KEY=
Add .env to your .gitignore immediately, before you write another line. A private key committed to a public repository, even for a few minutes, should be treated as burned and the wallet should be migrated to a fresh address.
Step 3: Scan a Wallet for Active Approvals
Etherscan’s API v2 exposes an endpoint for pulling ERC-20 Approval event logs by address. Rather than replaying the entire chain, you query the event logs directly, filtering for the Approval topic hash. Save this as scripts/fetchApprovals.js.
// scripts/fetchApprovals.js
require('dotenv').config();
const axios = require('axios');
const APPROVAL_TOPIC =
'0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925';
const { ETHERSCAN_API_KEY, WALLET_ADDRESS } = process.env;
async function fetchApprovals(chainId = 1) {
const url = 'https://api.etherscan.io/v2/api';
const paddedAddress =
'0x' + WALLET_ADDRESS.slice(2).padStart(64, '0');
const { data } = await axios.get(url, {
params: {
chainid: chainId,
module: 'logs',
action: 'getLogs',
fromBlock: 0,
toBlock: 'latest',
topic0: APPROVAL_TOPIC,
topic1: paddedAddress,
topic0_1_opr: 'and',
apikey: ETHERSCAN_API_KEY,
},
});
if (data.status !== '1') {
console.warn(`Chain ${chainId}: ${data.message}`);
return [];
}
return data.result;
}
module.exports = { fetchApprovals };
Run it against a chain ID (1 for Ethereum mainnet, 137 for Polygon, 42161 for Arbitrum) and you’ll get back a raw log array. Each entry contains the token contract address, the spender that was approved, and the block where it happened. This is your full history, not just what’s currently active, so the next step matters.
Step 4: Score and Flag High-Risk Approvals
A raw approval log tells you what was granted, not what’s still live or how risky it is. You need to check the current allowance for each unique token/spender pair and rank the results. Save this as scripts/riskScore.js.
// scripts/riskScore.js
const { ethers } = require('ethers');
const ERC20_ABI = [
'function allowance(address owner, address spender) view returns (uint256)',
'function symbol() view returns (string)',
];
const MAX_UINT256 = 2n ** 256n - 1n;
async function scoreApproval(provider, owner, token, spender) {
const contract = new ethers.Contract(token, ERC20_ABI, provider);
const current = await contract.allowance(owner, spender);
if (current === 0n) return null; // already revoked or never confirmed
let risk = 'low';
if (current >= MAX_UINT256 / 2n) risk = 'critical'; // unlimited-style approval
else if (current > 0n) risk = 'medium';
const symbol = await contract.symbol().catch(() => 'UNKNOWN');
return { token, symbol, spender, allowance: current.toString(), risk };
}
module.exports = { scoreApproval };
Run this across every unique pair from Step 3 and sort by risk. In practice, a wallet that’s been active for a year or two on a few DEXs and bridges will typically surface somewhere between 10 and 40 unique approvals, and it’s common for a third of them to still show a full MAX_UINT256 allowance from a swap made long ago on a router you’ve since forgotten about.
Step 5: Revoke Manually with Revoke.cash
If you’d rather skip the scripting entirely, the manual path works fine for a single wallet and takes about ten minutes. Revoke.cash and Etherscan’s own token approval checker both do this without you writing any code.
- Go to revoke.cash and connect your wallet using the button in the top right.
- Select the network you want to audit from the dropdown. Approvals are tracked per chain, so you’ll need to repeat this for each one you’ve used.
- Review the list of tokens and their spenders. Each row shows the current allowance and the contract it was granted to.
- Click “Revoke” next to any entry you don’t recognize, no longer use, or that shows an unlimited allowance you didn’t intend to grant.
- Confirm the transaction in your wallet. This is an on-chain write, so it costs gas, and you’re paying it separately for every revoke unless you batch them (covered in the advanced tips below).
MetaMask’s own documentation walks through a near-identical flow using its built-in Portfolio approvals view, which currently covers Ethereum mainnet, Polygon, and BNB Chain natively. For everything else, a third-party checker or the relevant block explorer’s approval tool fills the gap.
Step 6: Revoke Programmatically with ethers.js
For auditing more than one wallet, or for building this into a recurring job, scripting the revoke call is faster than clicking through a UI each time. Revoking an ERC-20 approval means calling approve(spender, 0), the same function used to grant it in the first place, just with the amount zeroed out.
// scripts/revoke.js
require('dotenv').config();
const { ethers } = require('ethers');
const ERC20_ABI = [
'function approve(address spender, uint256 amount) returns (bool)',
];
async function revokeApproval(rpcUrl, tokenAddress, spenderAddress) {
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, wallet);
const tx = await contract.approve(spenderAddress, 0n);
console.log(`Revoke tx sent: ${tx.hash}`);
const receipt = await tx.wait();
console.log(`Confirmed in block ${receipt.blockNumber}`);
return receipt;
}
module.exports = { revokeApproval };
Only load a private key into a script you fully control, and preferably one running locally, not on a shared server. If you use a hardware wallet, swap the ethers.Wallet line for a hardware signer connection instead of ever exporting the raw key.
Step 7: Handle NFT setApprovalForAll Separately
NFT operator approvals use a different function signature and a boolean instead of an amount, so they need their own revoke call. This is the exact mechanism attackers exploited in the Safe{Wallet} phishing campaign that Blockaid documented, where roughly 15,000 lookalike proxy addresses were planted in victims’ wallet interfaces to harvest exactly this kind of blanket approval.
// scripts/revokeNFT.js
require('dotenv').config();
const { ethers } = require('ethers');
const ERC721_ABI = [
'function setApprovalForAll(address operator, bool approved)',
'function isApprovedForAll(address owner, address operator) view returns (bool)',
];
async function revokeNFTApproval(rpcUrl, collectionAddress, operatorAddress) {
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const contract = new ethers.Contract(collectionAddress, ERC721_ABI, wallet);
const tx = await contract.setApprovalForAll(operatorAddress, false);
console.log(`NFT operator revoke tx sent: ${tx.hash}`);
await tx.wait();
}
module.exports = { revokeNFTApproval };
Marketplaces like OpenSea and Blur both request setApprovalForAll the first time you list an item, and most users grant it once and never look at it again. Treat every NFT collection you’ve ever listed as a candidate for this check, not just the ones you actively trade.
Step 8: Verify the Revocation On-Chain
Don’t trust a green confirmation toast alone. Re-query the allowance after the transaction confirms to make sure the change actually landed, especially on chains prone to reorgs or if you sent the transaction with a low gas price during congestion.
// scripts/verify.js
const { ethers } = require('ethers');
const ERC20_ABI = [
'function allowance(address owner, address spender) view returns (uint256)',
];
async function verifyRevoked(provider, owner, token, spender) {
const contract = new ethers.Contract(token, ERC20_ABI, provider);
const remaining = await contract.allowance(owner, spender);
console.log(remaining === 0n ? 'Revoked successfully' : `Still allowed: ${remaining}`);
return remaining === 0n;
}
module.exports = { verifyRevoked };
Sample output from a clean run against a wallet with three stale approvals looks like this:
$ node scripts/audit.js
Scanning 0x7a2f...c391 across 3 chains...
Found 14 unique approvals, 5 flagged critical (unlimited)
Revoking USDC -> 0x1111...routerA ... tx 0x9ab2... confirmed block 21894213
Revoking WETH -> 0x2222...routerB ... tx 0x7cd4... confirmed block 21894215
Revoking NFT operator (BoredCollection) -> 0x3333...market ... tx 0x1fe0... confirmed block 21894219
Verifying...
Revoked successfully
Revoked successfully
Revoked successfully
Done. 5 critical approvals down to 0.
Step 9: Set Up Ongoing Monitoring
A one-time cleanup solves today’s problem, not next quarter’s. New approvals accumulate every time you use a new dapp, so wire the scanner from Step 3 into a recurring job rather than remembering to run it manually.
# crontab -e
# Run the audit every Monday at 9am and log anything flagged
0 9 * * 1 cd /home/you/approval-auditor && node scripts/audit.js >> audit.log 2>&1
If cron isn’t your style, our guide on tracking crypto wallet alerts covers setting up address-watching services that can ping you the moment a new, unusually broad approval gets granted from your address, which catches the problem before the scanner would even run again. “Unusually broad” here means anything close to the max uint256 value, or any setApprovalForAll call, since both are the patterns that show up in nearly every drainer post-mortem. A narrow, capped approval to a contract you actually use rarely needs a second look.
Step 10: Clean Up Across Every Chain You’ve Used
Revoking on Ethereum mainnet does nothing for the identical approval you granted on Arbitrum, Base, Polygon, or BNB Chain. Each EVM chain maintains its own state, so an approval is scoped entirely to the chain it was signed on. Run through every network your wallet has touched.
| Chain | Chain ID | Typical gas per revoke | Notes |
|---|---|---|---|
| Ethereum Mainnet | 1 | Highest | Oldest approvals tend to live here |
| Arbitrum One | 42161 | Low | Common for perps and derivatives approvals |
| Base | 8453 | Low | Fast-growing, check recent dapp activity |
| Polygon PoS | 137 | Very low | Often has the most forgotten approvals |
| BNB Chain | 56 | Low | Covered natively by MetaMask Portfolio |
If you’re not sure which chains a given address has touched, a block explorer’s multichain search (Etherscan’s API v2 supports querying by chain ID in a single loop, as shown in Step 3) is faster than checking each one by hand.
Budget for the full cleanup realistically. A wallet with moderate activity across three or four chains usually turns up somewhere between 15 and 30 approvals worth reviewing, and revoking the critical ones typically takes 20 to 45 minutes end to end once you include wallet confirmations and the occasional slow block on a congested network. Mainnet revokes cost the most, often several dollars each during normal conditions, while L2 revokes on Arbitrum, Base, or Polygon usually run a few cents. Running the scan first and revoking only what’s flagged critical keeps both the time and the gas bill manageable instead of blindly zeroing out everything in the list.
Comparing Revocation Methods
Each approach in this tutorial trades off coverage, cost, and effort differently. Here’s how they stack up.
| Method | Chains covered | Setup time | Best for |
|---|---|---|---|
| Revoke.cash (manual) | 40+ EVM chains | ~2 minutes | One-off cleanup on a single wallet |
| MetaMask Portfolio | Ethereum, Polygon, BNB Chain | 0, built in | Quick check without leaving the wallet |
| Block explorer approval checker | Per-explorer chain | ~2 minutes | Verifying a specific token/spender pair |
| Custom ethers.js script (this guide) | Any chain with an RPC | ~20 minutes | Auditing multiple wallets or recurring jobs |
None of these four are mutually exclusive. A reasonable workflow is to run the manual method once to get a feel for what a clean wallet looks like, then move to the scripted version once you’re managing more than one address or want the process to repeat on its own. Developers auditing a client’s wallet or a company treasury should default to the script from the start, since it produces a log you can hand back as evidence the check was actually done.
Common Pitfalls When Revoking Token Approvals
These are the mistakes that show up most often, both in support threads and in wallets we’ve reviewed while researching this guide.
- Treating revocation as a full fix after a compromise. If your seed phrase or private key leaked, revoking approvals doesn’t help. Move funds to a fresh wallet first, then worry about approvals on the old one.
- Forgetting approvals are per chain. Revoking on mainnet leaves the same allowance live on every L2 you’ve bridged to.
- Confusing “disconnect” with “revoke.” Disconnecting a dapp in your wallet’s connections list only stops it from seeing your address, it does not touch any approval already granted.
- Missing Permit and Permit2 signatures. These off-chain signed allowances don’t appear as a transaction in your history, so a scanner that only reads on-chain
Approvalevents can miss them entirely. - Revoking a still-active position by mistake. If you have funds actively staked or supplied through a contract, zeroing its approval can block you from withdrawing until you re-approve, so check what a spender is used for before you kill it.
- Ignoring NFT operator approvals. A scan focused only on ERC-20 tokens will miss the
setApprovalForAllgrants that marketplaces request, which is exactly the vector used in the Safe{Wallet} campaign mentioned earlier.
Troubleshooting Guide
Most issues with this workflow come down to network mismatches, stale RPC endpoints, or a rate limit you didn’t know you’d hit. The list below covers the ones that come up most.
- Wallet won’t connect to Revoke.cash: Refresh the page, then check that your wallet extension isn’t locked or pointed at a network the site doesn’t support yet.
- Transaction stuck as pending: The gas price was likely set too low during network congestion. Speed it up from your wallet’s activity tab, or cancel and resend with a higher gas price.
- Etherscan API v2 returns “NOTOK”: Usually means the API key wasn’t included, is invalid, or you’ve hit the free-tier rate limit of 5 calls per second, add a short delay between chain loops.
- “Insufficient funds for gas” when revoking: You need native gas token on that specific chain. A wallet full of tokens on Polygon still needs MATIC (or POL) to pay for the revoke transaction there.
- Script throws “invalid address” or “invalid BigNumberish”: Almost always a copy-paste error in the token or spender address, or passing a string where the script expects a BigInt (note the trailing
nin values like0n). - Approval reappears after you thought you revoked it: You likely interacted with that same dapp again after revoking, which silently re-approved it. Check your recent transaction history for a second approve call.
- NFT approval doesn’t show up in the scan: Confirm you’re querying the
ApprovalForAllevent topic, not the single-tokenApprovaltopic, they’re different event signatures entirely. - A chain you use isn’t supported by your chosen tool: Fall back to that chain’s own block explorer if it runs Etherscan’s software, or use the custom script from Steps 3 and 6 with the correct RPC URL and chain ID.
Advanced Tips: Batch Revocation, Permit2, and EIP-7702
Once the basic flow works, a few refinements save gas and close gaps the standard approach misses.
Batch your revokes with a multicall contract. Sending ten separate approve(spender, 0) transactions costs ten times the base gas fee. A multicall wrapper bundles them into a single transaction, which is worth building into the script above once you’re revoking more than four or five approvals at once.
Check Permit2 separately. Uniswap’s Permit2 contract acts as a universal approval layer that many newer dapps route through instead of asking for a direct ERC-20 approval. Revoking the underlying token approval to Permit2 itself is often more effective than trying to track every individual dapp that uses it, since Permit2 stores its own internal allowance state per spender.
Watch EIP-7702 delegations. With account abstraction features from the Pectra and Fusaka upgrades now live, EOAs can temporarily delegate execution to a smart contract. This is a legitimate feature for smart-account wallets, but it’s also a newer surface attackers are exploring, since a malicious delegation can behave like a blanket approval without looking like one in a traditional approval scan.
Sign revokes with a hardware wallet when the stakes are high. For any wallet holding meaningful value, route the signing step through a hardware device rather than a hot wallet private key, even for a routine cleanup. If you haven’t reviewed your hardware wallet’s own security posture recently, our hardware wallet security guide covers firmware verification and other checks worth running alongside this audit.
Put it on a calendar, not just a cron job. Automated scanning catches new approvals, but a quarterly manual review catches the judgment calls a script can’t make, like whether a spender you don’t recognize is actually a rebrand of a protocol you still use.
Building the Complete Project
Put together, the six scripts above form a small but complete approval-auditor project. The folder structure looks like this once everything from this tutorial is in place.
approval-auditor/
.env
.gitignore
package.json
scripts/
fetchApprovals.js // Step 3
riskScore.js // Step 4
revoke.js // Step 6
revokeNFT.js // Step 7
verify.js // Step 8
audit.js // orchestrates all of the above, run via cron
The audit.js entry point ties the pieces together: it fetches approvals per chain, scores each one, prints anything flagged critical, and (optionally, behind a confirmation flag) calls the revoke functions automatically. That’s the same script referenced in the sample output in Step 8 and the cron job in Step 9.
// scripts/audit.js
require('dotenv').config();
const { ethers } = require('ethers');
const { fetchApprovals } = require('./fetchApprovals');
const { scoreApproval } = require('./riskScore');
const { revokeApproval } = require('./revoke');
const { verifyRevoked } = require('./verify');
const CHAINS = [
{ id: 1, rpc: process.env.RPC_URL_MAINNET, name: 'Ethereum' },
{ id: 137, rpc: process.env.RPC_URL_POLYGON, name: 'Polygon' },
{ id: 42161, rpc: process.env.RPC_URL_ARBITRUM, name: 'Arbitrum' },
];
const AUTO_REVOKE = process.argv.includes('--revoke');
async function run() {
for (const chain of CHAINS) {
const provider = new ethers.JsonRpcProvider(chain.rpc);
const logs = await fetchApprovals(chain.id);
console.log(`\n${chain.name}: ${logs.length} approval events found`);
const seen = new Set();
for (const log of logs) {
const token = ethers.getAddress('0x' + log.topics[0].slice(-40));
const spender = ethers.getAddress('0x' + log.topics[2].slice(-40));
const key = `${token}-${spender}`;
if (seen.has(key)) continue;
seen.add(key);
const result = await scoreApproval(provider, process.env.WALLET_ADDRESS, token, spender);
if (!result) continue;
console.log(` [${result.risk}] ${result.symbol} -> ${spender}`);
if (result.risk === 'critical' && AUTO_REVOKE) {
await revokeApproval(chain.rpc, token, spender);
await verifyRevoked(provider, process.env.WALLET_ADDRESS, token, spender);
}
}
}
}
run();
Run node scripts/audit.js alone to get a dry-run report, or add the --revoke flag once you’ve reviewed the output and are ready to let it act automatically on anything flagged critical. From here, the natural next step is wiring in a Telegram or email notification instead of just a log file, so the weekly cron run actually reaches you instead of sitting in a text file you forget to check. For general wallet hygiene beyond approvals specifically, our broader crypto wallet security walkthrough and seed phrase security guide are worth running through alongside this one.
Beyond Revocation: Other Defenses Worth Layering On
Revoking stale approvals closes one door, but it’s one piece of a wider defense. Wallet-level transaction simulation, the kind now built into MetaMask’s security stack and offered as a browser extension by Blockaid, flags a malicious transaction before you sign it rather than after the fact. That matters because a scanner like the one in this tutorial is inherently reactive, it tells you what already happened, not what’s about to.
Address-book allowlisting helps with poisoning specifically. Most modern wallets let you save trusted addresses under a label, and some will warn you if you’re about to send to an address that merely resembles one in your recent history rather than matching it exactly, which is the entire trick behind a poisoning attempt. Turning that setting on costs nothing and catches the exact failure mode that cost the December 2025 victim $50 million.
Finally, treat a fresh wallet as a legitimate tool, not overkill. If an address has accumulated years of approvals across dozens of dapps, migrating meaningful holdings to a new address with a clean approval history and being more selective about what gets approved going forward is sometimes faster than auditing years of history. Keep the old address around for monitoring, but stop actively using it once the balance is moved.
Frequently Asked Questions
Does revoking a token approval cost gas?
Yes. A revoke is an on-chain state change (an approve call with the amount set to zero), so it requires a transaction and pays the same gas mechanics as any other write operation on that chain.
Is Revoke.cash safe to connect my wallet to?
Revoke.cash only requests read access to view your approvals and asks for a signature only when you choose to revoke something specific, it never asks for a blanket approval itself. As with any tool, verify you’re on the correct URL before connecting, since phishing clones of popular security tools do exist.
What’s the difference between disconnecting a dapp and revoking an approval?
Disconnecting stops a site from seeing your address in your wallet’s connected-sites list. It has no effect on any token approval you previously granted, that requires a separate on-chain revoke transaction.
How often should I audit my wallet’s approvals?
A quarterly manual review plus an automated weekly scan (as set up in Step 9) covers most users. Anyone actively testing new dapps or minting frequently should scan more often, since new approvals accumulate with every new contract you interact with.
Can revoking approvals undo a hack that already happened?
No. Revoking only prevents future transfers through that specific approval. If funds were already drained, revoking doesn’t recover them, and if your private key or seed phrase is compromised, revoking approvals on that wallet won’t stop the attacker from acting directly.
Why do some approvals show as “unlimited” instead of a specific number?
Many dapp interfaces request the maximum possible value (2^256 – 1) by default when you approve a token, since it means you won’t need to sign another approval the next time you use that same contract. It’s a convenience trade-off that also maximizes what an attacker could take if that contract is ever compromised.
Do I need to revoke approvals on a hardware wallet the same way?
Yes, the process is identical, you just confirm the transaction on the hardware device instead of a software wallet popup. The extra step of physically verifying the transaction details on the device screen is worth the few seconds it adds.
Will a token approval scanner catch every possible risk?
It catches on-chain Approval and ApprovalForAll events, plus most Permit2 usage if you check the Permit2 contract’s own allowance state. It won’t catch a compromised seed phrase, a malicious browser extension reading your clipboard, or a fake front-end tricking you into a fresh malicious signature, those need separate defenses layered on top.
What should I do first if I think I’ve already been drained?
Move any remaining funds and NFTs on that wallet to a brand-new address immediately, using a device you’re confident isn’t compromised. Revoking approvals on the drained wallet can wait until after the funds are safe, since an active drainer script can often act faster than you can click through a revoke transaction.




