The freeze switch under x402: auditing the USDC blacklist
An autonomous agent that pays per call holds a balance in an asset whose issuer can switch it off. We counted how often that switch is pulled, and traced what x402 does when it is.
The agent payment stack spends most of its attention on signatures. Which curve, which domain separator, which wallet type, which facilitator. That work is done and mostly correct. Underneath it sits a simpler question that almost nothing in the stack models: the token itself has an administrator, and that administrator can make an address unable to send or receive, permanently, in one transaction.
This is not a hypothetical. It is a function on the contract, it emits an event, and the event log is a public record of every time it has been used. So we read the contract, counted the events on Ethereum and Base, priced the immobilized balances, and then went into the x402 codebase to see what a facilitator actually reports when a payer is frozen mid-flow.
Everything below was measured on 2026-09-22 against mainnet.
The switch, in Solidity
USDC is a proxy over Circle's stablecoin-evm implementation. The relevant contract is Blacklistable: a blacklister role, a blacklist(address) and unBlacklist(address) pair guarded by onlyBlacklister, a public isBlacklisted(address) view, and two events, Blacklisted and UnBlacklisted.
The enforcement is a modifier. In FiatTokenV2_2, the version deployed on every major chain, the EIP-3009 entry points carry it explicitly:
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature
) external virtual whenNotPaused notBlacklisted(from) notBlacklisted(to) {
// Blacklistable.sol
modifier notBlacklisted(address _account) {
require(
!_isBlacklisted(_account),
"Blacklistable: account is blacklisted"
);
_;
}
Three details matter for machine payments.
First, the gate covers from and to — the payer and the payee — but not msg.sender. A relaying facilitator that is itself blacklisted can still submit transferWithAuthorization for other people. It just cannot hold or move its own USDC, because transfer and transferFrom in FiatTokenV1 do check msg.sender. The relayer role survives a listing; the treasury role does not.
Second, in V2_2 the blacklist flag is not a separate mapping. It is the high bit of the packed balance slot: _setBlacklistState ORs 1 << 255 into balanceAndBlacklistStates[account], and _setBalance reverts with "FiatTokenV2_2: Account is blacklisted" if that bit is set. Balance and censorship live in the same storage word. The balance stays readable — balanceOf masks the bit off — which is why the immobilized amounts below are countable at all.
Third, initializeV2_2 blacklists address(this). We confirmed it on Base: isBlacklisted(0x8335…2913) on the USDC contract itself returns true. The token refuses to receive itself.
A live probe on Base
Modifier order decides what an agent sees. We picked a currently listed address on Base and called transferWithAuthorization with a deliberately invalid signature, then repeated the call with a clean address, both via eth_call against Base mainnet:
// from = 0xd882cfc20f52f2599d84b8e8d58c7fb62cfe344b (listed on Base)
execution reverted: Blacklistable: account is blacklisted
// from = a never-listed address, same junk signature
execution reverted: ECRecover: invalid signature
The blacklist check runs before signature recovery. That is good news for anyone who wants to pre-screen: a single eth_call to isBlacklisted — or a plain simulation — tells you the payer is unusable before you spend a signature, a nonce, or gas on it. It is also the reason the failure is easy to miss: the revert string never mentions the payer's signature, the amount, or the nonce, so a generic error handler files it under "the transfer reverted".
The census: 833 addresses on Ethereum, 639 still listed
We pulled the full Blacklisted and UnBlacklisted log history for Ethereum USDC (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) from before deployment to head, replayed the events in block order to get the current state, then read every live balance.
887 listings, 224 reversals, 639 addresses currently frozen
887 Blacklisted events across 833 distinct addresses. First listing 2020-06-16 (block 10,274,701), most recent 2026-09-15. 224 UnBlacklisted events across 194 addresses. Net currently listed: 639.
Balance held by those 639 addresses: 120,734,323.95 USDC. Only 147 of them hold anything at all; 492 are frozen at zero. The two largest hold 30.1M and 27.7M.
The cadence is the interesting part. Listings per year: 1 in 2020, 24 in 2021, 126 in 2022, 60 in 2023, 54 in 2024, 215 in 2025, and 407 in 2026 through 15 September. Whatever the policy is, its application rate has roughly doubled year over year since 2024, and 2026 is already triple 2025.
It is also bursty, not continuous. The five largest single days on Ethereum are 2026-07-01 (131 addresses), 2026-03-23 (80), 2025-11-04 (74), 2022-11-10 (62) and 2026-09-09 (54). The 2026-03-23 batch landed inside a 46-minute window. Monthly, 2026 reads: 11, 1, 109, 19, 18, 3, 160, 31, 55. An agent operator monitoring this looks at a process that is idle for weeks and then lists a hundred addresses in an afternoon.
Reversals are real but concentrated: 125 of the 224 unlistings happened in 2025 and 88 in 2026, including 56 in May 2026 alone. Being listed is not always terminal — but the reversal is an administrative act on someone else's timeline, not something a payer can trigger.
Base is a smaller, newer, and busier list
Base is where most x402 traffic settles, so we ran the same census on 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913.
565 Blacklisted events, 562 distinct addresses, 93 reversals, 469 currently listed, holding 2,371,161.96 USDC — and only 23 of the 469 have a non-zero balance. The first day of the list is 2023-08-18, the day Base USDC started emitting: 141 addresses in one batch, which is the Ethereum list being seeded onto a new chain rather than 141 independent decisions.
Since then: 27 listings in 2024, 125 in 2025, 254 in 2026. The busiest days are 2026-07-01 (131), 2026-09-09 (52) and 2026-08-24 (22). Base carries roughly 2% of Ethereum's frozen dollar value but a similar number of listed addresses — consistent with Base being where small, fast, disposable addresses live. Which is exactly what an agent wallet is.
The list is per-chain, and only mostly synchronized
Circle's policy says it can block addresses "on every blockchain to which Circle Stablecoin is issued". The contracts, though, hold independent state, and the logs show the difference between capability and practice.
Of the addresses we saw listed, 545 appear on both Ethereum and Base, 288 appear only on Ethereum, and 17 only on Base. The Ethereum-only set is not just legacy: 253 of those 288 were listed after Base's list existed, including 152 during 2026. The clearest example is 2026-03-23 — 80 addresses listed on Ethereum, zero on Base that day.
When propagation does happen, it is fast. Across the 544 shared addresses with comparable timestamps, the median gap between the Ethereum listing and the Base listing is 190 seconds, and 409 of them were listed on both chains within the same hour. On 2026-07-01 the same 131 addresses were listed on both chains inside overlapping four-hour windows.
maxTimeoutSeconds to 300 (the EVM exact spec's examples use 60). A payer can be clean at verify and listed at settle inside one payment.
We also read the admin roles. Each chain has its own blacklister, and all six we checked are plain EOAs with no code: Ethereum 0x0a06…78f9, Base 0x1f2e…e277, Polygon 0xe99c…95b6, Arbitrum 0xac5b…1329, Avalanche 0x00fe…d572, Celo 0x4a63…ca12. None of the six contracts is paused, and rescuer() is the zero address on all of them.
What Circle actually promises in writing
The governing document is the Circle Stablecoin Access Denial Policy, referenced from the USDC risk factors page. It is two pages and worth reading in full. The structure is a default plus exceptions: Circle "will not deny access to individual addresses, other than in circumstances that strictly conform to" three exceptions — a threat to the integrity of the network (for example a compromised minter key), compliance with "a law, regulation, or legal order from a duly recognized U.S. or French authorized authority", and urgent law-enforcement or sanctions requests pending a valid court order.
A footnote narrows the forum considerably: "U.S. courts of competent jurisdiction are in Delaware or Massachusetts." Reversal is available only "upon formal confirmation" from the same authority that the order has been lifted. There is no notice requirement to the address holder, no stated timeframe, and no appeal path in the document.
Section 5 is a commitment: "Circle will regularly report publicly the most up-to-date amount of access denied Circle Stablecoin tokens." We could not find that figure published on Circle's transparency page, which covers reserves and attestations. The number in this post — roughly 123.1M USDC immobilized across Ethereum and Base — is our own count from the event log, and anyone can reproduce it with two getLogs calls and a batch of balanceOf reads.
Where x402 is blind
We cloned x402-foundation/x402 at HEAD 279f12c (2026-09-22) and followed the EVM exact path end to end.
The good news: verifyEIP3009 simulates by default. A listed payer fails verification rather than being discovered at settlement. The bad news is what the failure is called.
When the simulation reverts, the facilitator calls diagnoseEip3009SimulationFailure, which multicalls four reads — balanceOf, name, version, authorizationState — and maps them to specific reasons: nonce already used, token name mismatch, version mismatch, insufficient balance, EIP-3009 unsupported. There is no isBlacklisted read in that list. A frozen payer passes every diagnostic (the balance is intact and readable) and falls through to the generic invalid_exact_evm_transaction_simulation_failed.
At settlement the same gap appears in string form. parseEip3009TransferError matches five regular expressions — expired authorization, not-yet-valid, nonce used, insufficient balance, invalid signature — and returns invalid_exact_evm_transaction_failed for anything else. "Blacklistable: account is blacklisted" matches none of them. And simulateInSettle defaults to false, so a payer listed between verify and settle burns the facilitator's gas on a reverting transaction and reports a generic failure.
// what the agent sees for a permanently frozen payer
{ "isValid": false,
"invalidReason": "invalid_exact_evm_transaction_simulation_failed",
"payer": "0x…" }
// indistinguishable from: RPC hiccup, paused token, bad node,
// transient reorg — all of which are worth retrying. This is not.
That is the whole problem in one field. The exact-EVM error module exports 60 reason codes, 19 of them Permit2 variants, and none of them means "this payer can never pay". An agent with a retry-with-backoff loop will keep retrying a wallet that will never work again, and a gateway that demotes a payer after N transient failures will treat a permanent condition as flakiness.
The spec is silent too. scheme_exact_evm.md is 2,221 words and contains zero occurrences of blacklist, freeze or frozen. The TON exact scheme explicitly rejects frozen accounts and the SVM batch-settlement spec handles frozen settlement accounts; the EVM path, which carries almost all live x402 volume, does not mention issuer freeze as a failure mode at all.
Bridged USDC: the opposite failure mode
The other side of the switch is where it does not exist. Circle's Third-Party Bridged USDC Terms state plainly: "Circle does not control the Bridged USDC contract on the Supported L2 Networks and Circle lacks the ability to block certain addresses or freeze Bridged USDC in the event your funds are stolen."
x402's EVM default asset table mixes both worlds. It lists native USDC on Ethereum, Base, Polygon, Arbitrum, Avalanche and Celo, alongside entries labelled "Bridged USDC", "USDC.e", USDT0 and several chain-specific dollars. Those are different assets with different administrators and different failure modes: on native USDC the issuer can freeze you, on bridged USDC nobody can freeze anyone — including when your own funds are stolen. Neither property is expressed anywhere in the x402 asset metadata, which carries name, version, decimals and symbol. We covered the analogous blocklist on USDT0's EIP-3009 path in the Tether WDK audit; the mechanics differ per issuer, and nothing in the protocol tells a buyer which one they are about to sign against.
What it means for LLM4Agents
We settle inference in USDC over EIP-3009, mostly on Base. That means three distinct exposures, and they are not equally bad.
The first is the payer. An agent's wallet gets listed and every subsequent request from it fails. This is the mild case: the loss is bounded by the agent's own balance, and our gateway's job is to report it correctly and stop charging retries against it. Today, with the stock facilitator error taxonomy, we cannot tell it apart from a bad RPC.
The second is the payee — our own payTo address. The modifier gates to as well as from. A listing on a receiving address does not degrade service, it stops all inbound settlement on that chain at once, for every agent, with signed authorizations already in flight that can no longer be settled. This is the one that deserves engineering: a single receiving address is a single point of failure that a third party can pull without notice.
The third is float. 120.7M USDC sits immobilized on Ethereum, and the concentration is extreme: 147 of 639 listed addresses hold anything at all, and two of them hold 48% of the total. Value parked in an address is the entire exposure; value that moves through it within seconds is almost none. Our reserve-proxy-settle design already keeps balances short-lived, which is the correct posture here for reasons that have nothing to do with capital efficiency.
The regulatory direction reinforces all three. As we wrote in the GENIUS Act analysis, the compliance trajectory for payment stablecoins runs toward issuers having — and using — the technical ability to act on lawful orders. The 2026 listing rate is what that looks like in the event log.
Staying on the frontier
Five concrete steps, in the order we would do them.
1. Monitor our own addresses. A watcher subscribed to Blacklisted on every chain where we hold a payTo or a treasury address, alerting within one block. The event is indexed by address, so the filter is exact and cheap. This is a day of work and it converts a silent outage into a page.
2. Make the payee replaceable. Per-chain receiving addresses, rotated on a schedule, with the payment requirements generated from a registry rather than a constant. If one payTo is listed, the next 402 response advertises another. Rotation also keeps any single receiving address from accumulating history worth acting against.
3. Pre-screen and classify. Add an isBlacklisted read to the facilitator's diagnostic multicall — it is one extra call in a batch that already makes four — and a distinct reason code, so a frozen payer is reported as permanent and not retried. We will upstream this as three changes to x402: the diagnostic branch, a "Blacklistable: account is blacklisted" pattern in parseEip3009TransferError, and a failure-modes section in scheme_exact_evm.md. It is the smallest patch in this post and the one with the widest blast radius, since every facilitator running the reference implementation inherits the blind spot. See the facilitator anatomy post for where these hooks sit.
4. Enable simulateInSettle above a value threshold. The default of false is right for a $0.001 inference call and wrong for a batched settlement. The 190-second median propagation we measured is the width of the window this closes.
5. Publish the asset's administrative profile. Our /supported-style metadata should say, per asset, who can freeze it and who cannot: native USDC (issuer can freeze payer and payee), bridged USDC (nobody can, including on theft), USDT0 (different blocklist, different path). A buyer agent choosing between two rails should be able to read that from the payment requirements instead of from a blog post. This is the same argument we made about trust signals in the agent threat model: unstated administrative power is still power.
None of this makes the switch go away. USDC is a regulated liability and the freeze function is a feature of that, not a bug in it. What is fixable is the part that is ours: an agent should never confuse a permanent block with a flaky node, and a gateway should never let one third-party decision take down every inbound payment at once.
Pay per call, hold nothing
Short-lived balances, per-request settlement, and an OpenAI-compatible gateway that tells your agent exactly why a payment failed.
Register an agent