EIP-7702 smart EOAs on x402: who validates the signature
Your agent's wallet signs a payment the same way it always has. But if that wallet has been upgraded with EIP-7702, a different function decides whether the signature is valid — and the contract that decides is not one you chose.
The mechanic is small enough to fit in a sentence. EIP-7702, a Final core EIP authored by Vitalik Buterin, Sam Wilson, Ansgar Dietrichs and lightclient (created May 7, 2024), adds transaction type 0x04 with an authorization tuple [chain_id, address, nonce, y_parity, r, s]. When it executes, in the spec's words, it will "Set the code of authority to be 0xef0100 || address. This is a delegation indicator." Twenty-three bytes: a three-byte prefix and a twenty-byte delegate address.
Those twenty-three bytes are the entire subject of this audit. Every stablecoin payment path an agent uses — EIP-3009 transferWithAuthorization, Permit2, batch-settlement deposits — asks the same question before it moves money: does this address have code? Before delegation the answer was no, and the token ran ecrecover. After delegation the answer is yes, and the token calls isValidSignature on whatever contract the wallet vendor pointed at.
As always in this series, everything below is primary-source. We read the specs and the contracts, cloned the x402 monorepo at HEAD 8468e3a (August 26, 2026), scanned 24 hours of Base mainnet, and drove the real USDC verifier against every delegate implementation we found. All measurements are ours, taken August 27, 2026.
The router inside the token
Start with the token, because the token has the last word. Circle's stablecoin-evm ships a util/SignatureChecker.sol whose decision is a single branch: if (!isContract(signer)) recover the key with ECRecover.recover, otherwise call ERC-1271 isValidSignature and compare the result against IERC1271.isValidSignature.selector. USDC v2.2 documents the consequence directly in the natspec of transferWithAuthorization, receiveWithAuthorization and permit: the signature may come from an EOA wallet or a contract wallet.
That branch reads extcodesize, and a 7702 delegation designation is code. So a delegated EOA takes the contract path — not because anyone decided it should, but because 23 bytes exist at the address.
We confirmed the domain we would be signing against before touching anything else. Base USDC (0x8335…2913) returns DOMAIN_SEPARATOR() = 0x02fa7265e7c5d81118673727957699e4d68f74cd74b7db77da710fe8a2c7834f, which matches the value we computed locally from name USD Coin, version 2, chainId 8453 and the token address. Same domain, same digest, no ambiguity in anything that follows.
What x402 did about it
The x402 monorepo took this seriously in June. PR #2658, "feat: improve & document wallet compatibility" by Carson Roscoe, was opened June 17, 2026 and merged June 25, 2026: 115 files, +12,115/−3,018, 23 commits. It shipped in @x402/evm 2.17.0 with a changelog entry promising that "payments verify and settle consistently across plain EOAs, deployed smart accounts (ERC-4337 / ERC-7579), counterfactual ERC-6492 wallets, and ERC-7702-delegated EOAs".
The core of it is one file, typescript/packages/mechanisms/evm/src/shared/verifySignature.ts, which reimplements the on-chain branch in TypeScript and refuses to be more permissive than it:
// if signer.code.length == 0:
// ecrecover(digest, sig) == signer
// else:
// IERC1271(signer).isValidSignature(digest, sig) == 0x1626ba7e
export function verifyHashSignatureWithCode(signer, address, code, digest, signature) {
if (!code || code === "0x") {
return verifyECDSA(address, digest, signature);
}
return verifyERC1271(signer, address, digest, signature);
}
The comment above it names the regression that motivated the work: the strict primitive "deliberately does NOT fall back to ECDSA when EIP-1271 returns failure. That fallback (which viem's publicClient.verifyTypedData performs) makes pre-verify accept signatures that on-chain rejects — most visibly for ERC-7702 delegated EOAs whose delegate's isValidSignature does not accept raw owner ECDSA."
The Go SDK carries the same history in verify_universal.go: "The old 65-byte EOA fast-path (that skipped GetCode) was removed because it caused pre-verify to accept signatures that on-chain verifiers routed to isValidSignature and rejected." Python mirrors it as verify_typed_data_strict. Three SDKs, one rule: the facilitator's verify should never say yes to something settle will reject.
The PR also added docs/advanced-concepts/wallet-compatibility.mdx, 155 lines that name five wallet types. A is a plain EOA. B is a deployed smart account. C is a counterfactual ERC-6492 wallet. D and E are both 7702-delegated EOAs, split by what the delegate accepts: D is a "permissive delegate" that takes raw owner ECDSA, E is a "strict delegate" that "requires a wrapped or prefixed format". The support matrix marks D as supported across every scheme and E as unsupported across every scheme, with a footnote that is admirably blunt: "x402 signs with signTypedData, which produces a raw 65-byte ECDSA signature that a strict delegate rejects, so the payment fails on-chain even though signing succeeded. This is the verifier behaving correctly, not an x402 limitation."
That is the whole design tension. x402's client (src/exact/client/eip3009.ts) calls signer.signTypedData and hands over 65 bytes. Whether those 65 bytes are money depends on a contract chosen by whoever upgraded the wallet.
isValidSignature(digest, rawECDSASig) on a 7702 address: 0x1626ba7e means permissive (Type D), 0xffffffff or a revert means strict (Type E). Nobody had run that test across the production population. So we did.
Census: who actually pays with EIP-3009 on Base
We scanned blocks 50,472,851 to 50,516,051 on Base — 43,200 blocks, roughly 24 hours — for AuthorizationUsed(address,bytes32) on USDC. That is the event every EIP-3009 payment emits, the settlement primitive we described in our EIP-3009 deep dive.
The window contains 74,029 authorizations across 73,991 distinct transactions, signed by 6,687 distinct addresses. We then called eth_getCode on every one of those 6,687 addresses. The split:
- 6,125 addresses (91.6%) have no code — plain EOAs, Type A.
- 497 addresses (7.4%) hold exactly 23 bytes starting with
0xef0100— 7702 smart EOAs, Type D or E. - 65 addresses (1.0%) hold ordinary contract bytecode — deployed smart accounts, Type B.
Weighted by payments rather than addresses: 69,267 authorizations (93.6%) came from plain EOAs, 4,330 (5.9%) from 7702 accounts, 432 (0.6%) from deployed contracts. One in seventeen stablecoin authorizations on Base is already signed by an account whose signature rules are set by a delegate contract.
Those 497 accounts point at 18 distinct delegate implementations. Ranked by account count, with names taken from verified sources on Base:
0x7702cb55…7176c EIP7702Proxy 243 accounts 708 auths
0x63c0c19a…ae32b EIP7702StatelessDeleGator 117 accounts 1,596 auths
0x955d8413…22c6f TKGasDelegate 48 accounts 76 auths
0x000066a0…30084 TKGasDelegate 32 accounts 49 auths
0x612373d7…951d3 CaliburEntry 13 accounts 51 auths
0x00000000…8f00 CaliburEntry 7 accounts 29 auths
0xd6cedde8…75b28 Kernel 7 accounts 33 auths
0xe40ccb2d…6fa4 SmartWalletEntry 6 accounts 15 auths
0xcc0c946e…6f17b TokenPocketSimple7702 5 accounts 50 auths
0xd2e28229…530fb Biz 5 accounts 5 auths
0x5a7fc113…6f6d AmbireAccount7702 3 accounts 4 auths
0xe6cae83b…555b Simple7702Account 2 accounts 1,698 auths
… plus six more with one account each (3 unverified)
The top entry, EIP7702Proxy, carries @author Coinbase (https://github.com/base/eip-7702-proxy) in its verified source: 243 accounts, 48.9% of the population. Second is MetaMask's EIP7702StatelessDeleGator, whose source path src/EIP7702/EIP7702StatelessDeleGator.sol places it inside the Delegation Framework we audited in the ERC-7710 rail audit. Uniswap's CaliburEntry appears at three separate addresses, ZeroDev's Kernel at one, Ambire at two, and the eth-infinitism reference Simple7702Account at one.
Two consumer wallet vendors hold two-thirds of the accounts. The long tail is where the surprises live.
The drive: 18 delegates, one raw signature
Knowing which delegate an account points at tells you nothing about whether it will accept a payment. So we ran the test the x402 docs prescribe — not on testnet mocks, but against the real Base USDC contract with the real code of each delegate.
The method: take a private key we control, build a genuine TransferWithAuthorization message for Base USDC, sign it with signTypedData exactly as the x402 client does (65 bytes), then run eth_call with a state override that sets our address's code to 0xef0100 || delegate. Public Base RPCs resolve delegation designations during execution, so the call runs the delegate's real bytecode in our account's context. We called transferWithAuthorization on USDC itself, with no balance, so the two outcomes are distinguishable: a revert with FiatTokenV2: invalid signature means the signature was rejected, and a revert with ERC20: transfer amount exceeds balance means it passed validation and failed only for lack of funds.
Thirteen of the eighteen delegates let the signature through and reverted on balance. Five rejected it with FiatTokenV2: invalid signature: Kernel, Biz, SemiModularAccount7702 and two unverified implementations. For all thirteen accepting delegates, a direct isValidSignature(digest, sig) call returned the magic value 0x1626ba7e — Type D by the book.
We also ran the ERC-7739 detection probe, the sentinel hash 0x7739…7739 with an empty signature that a supporting account answers with 0x77390001. Only Uniswap's CaliburEntry answered, at all three addresses — consistent with ERC7739Utils.sol appearing in its verified sources. Coinbase's proxy and SmartWalletEntry returned 0xffffffff; the rest reverted. ERC-7739 is still a Draft (created May 28, 2024) and its defensive rehashing is exactly the "wrapped format" the x402 doc warns about in Type E.
The correction the live accounts forced
A synthesized account has the delegate's code and empty storage. A real one has storage: an owner registered, a validator installed, a nonce tracker initialized. That difference is not cosmetic, and our own data caught it.
We took the 7702 accounts from the census, pulled their real transactions, decoded the calldata, recomputed the EIP-712 digest from the arguments, and called isValidSignature(realDigest, realSignature) on the live account. All ten delegates we could sample this way returned 0x1626ba7e. Among them were Kernel and two of the implementations that had rejected our synthetic signature — and in the sampled window, Kernel accounts settled three EIP-3009 authorizations through the legacy (v, r, s) overload, which is nothing but a 65-byte owner signature split into three fields.
So the honest finding is sharper than the docs' binary: Type D versus Type E is not a property of the delegate contract. It is a property of the delegate plus the account's storage at the moment of payment. The same Kernel bytecode rejects a raw owner signature when no validator is installed and accepts it once the account is initialized. An agent wallet can be Type E for the first minutes of its life and Type D afterwards — and a fleet operator who classifies wallets once, at provisioning time, will cache the wrong answer.
The signature shapes in production say the same thing. Across 184 direct EIP-3009 calls from 7702 accounts in our sample, 176 carried a plain 65-byte signature, five carried 224 bytes (the wrapped format Coinbase's stack produces), one carried 66 bytes (a one-byte validator prefix ahead of an ordinary signature, the ERC-7579 pattern), and two carried 640 and 736 bytes. The permissive path dominates because wallet vendors want their users' signatures to work everywhere — but the wrapped forms are already on the wire.
What this traffic actually looks like
The values are the part worth pausing on. Across those same 184 authorizations: median 0.005 USDC, 25th percentile 0.002, 75th percentile 0.01, maximum 18. One hundred and sixty-seven of 184 were under ten cents.
The single busiest 7702 payer in the window, 0x2b38a4bb…ab7d delegated to Simple7702Account, signed 1,689 authorizations in 24 hours. Every sampled one was exactly 0.005 USDC to the same recipient, broadcast by nine different relayer addresses. The second, 0x41cdc787…f3a5 on MetaMask's delegator, signed 741 — all 0.002 USDC, one recipient. Fixed price, one counterparty, many relayers, a payment every fifty seconds: that is not a human clicking. It is a machine on a meter, using precisely the primitive x402 standardizes, and it is running today with an upgraded EOA.
Three seams worth naming
First, the 7702 helpers in x402 are decorative. All three SDKs ship a detector — isERC7702Delegation in TypeScript, is_erc7702_delegation in Python, IsERC7702Delegation in Go — and each carries a docstring saying the helpers "are diagnostic only. The signature-verification path does not branch on 7702 detection". The TypeScript version is exported from the package index; we found no call site anywhere in the payment path. The SDK can tell you a payer is a smart EOA and never uses the fact — so there is no client-side pre-flight that warns a Type E wallet before it wastes a signature.
Second, the wallet matrix cannot run out of the box. The integration tests in TypeScript, Python and Go all define a "Wallet D — ERC-7702 EOA delegated to PermissiveECDSADelegate" case, and that contract name appears nowhere except in those test files: there is no such contract in the repo. The test skips unless an operator supplies a pre-delegated Base Sepolia key. The 7702 example client, meanwhile, delegates to Biconomy Nexus at 0x0000…3B03 with the comment that it "matches the 7702 → 7579 stack used by Privy-based wallets like Bankr" — an ERC-7579 account, the family the docs single out as the likeliest Type E.
Third, strictness costs a round-trip. Removing the 65-byte fast path means every verify now calls eth_getCode, including for the 91.6% of payers with no code at all. Against a public Base RPC we measured that call at a 119 ms median (114 ms min, 223 ms max over 15 calls). On a facilitator's hot path that is one extra network hop per payment, on top of the extra hop when the ERC-1271 branch fires. Correctness is worth it — the alternative is verify saying yes and settle reverting — but it is a real line item once a fleet is paying tens of thousands of times a day.
What it means for LLM4Agents
Our gateway takes stablecoin payments from agent wallets we do not control. This audit says the wallet's type is a first-class input to whether a payment will clear, and that type can change under us without any action on our side: a user opens a consumer wallet app, the vendor sends a type-0x04 transaction, and an account that was Type A yesterday is Type D or E today. Nothing in the payment payload changes. Nothing in the agent's code changes. The verifier's branch changes.
Three concrete consequences. One: our payer-side classification cannot be cached at registration. Given the storage-dependence we measured, the only reliable classification is the one taken against the account's current state, and the only truly definitive check is the transfer simulation itself. Two: the failure mode is silent from the agent's point of view. Signing succeeds, the payload is well-formed, and the rejection arrives as an invalid_signature-shaped error from a facilitator — the kind of dead end that turns into a retry storm in an autonomous loop. Three: the token matters as much as the wallet. The x402 doc warns that older ERC-3009 tokens use ecrecover only and never call isValidSignature, which inverts the whole picture: on those tokens a smart-account signature fails and only raw EOA keys work. Any asset we accept needs its signature routing verified, not assumed.
There is an upside worth stating just as plainly. A 7702 wallet keeps its key and its address while gaining code, which means it can hold a Permit2 allowance, use the gas-sponsoring extensions that deployed smart accounts cannot, and still sign transferWithAuthorization like an EOA — the "best of both" position we sketched when we looked at account abstraction for agent wallets. The 4,330 authorizations we measured in a single day are agents and bots already living in that position.
Staying on the frontier
Five moves, in order.
One: classify at payment time, not at signup. Add a wallet-type probe to the payment path — eth_getCode, then the 23-byte designation test, then the delegate address — and attach the result to the payment record rather than the account record. Cache it for seconds, not for the lifetime of the wallet.
Two: pre-flight the delegate before the first payment. Run the prescribed isValidSignature(digest, rawECDSASig) probe against the account's live state and surface the answer to the agent operator in plain language: this wallet's delegate accepts the signatures our client produces, or it does not. Both answers are actionable before money moves; neither is actionable after a settle revert. This is exactly the fact the SDK's diagnostic helpers already compute and then discard.
Three: keep a delegate registry with observed behavior. Eighteen implementations govern the entire population on one chain, and two of them cover two-thirds of it. A small internal table — delegate address, verified name, ERC-7739 detection result, observed accepted signature lengths — turns an open-ended compatibility question into a lookup with a fallback probe. Ours took a day of RPC calls to build.
Four: make the error legible to an autonomous caller. When a payment fails because a delegate rejected the signature format, the agent should get a distinct, non-retryable reason and a suggested remedy (initialize the account, install a default ECDSA validator, or pay from a different wallet), not a generic invalid-signature string. Retry loops are the expensive way to learn a wallet is Type E — and, as our spend-controls audit showed, error strings the model can read are the ones that shape its next move.
Five: track ERC-7739 adoption as the leading indicator. Uniswap's Calibur already answers the detection probe today. If defensive rehashing spreads to the two vendors that hold two-thirds of the population, the permissive default disappears and every client that signs raw typed data — x402's included — needs a wrapped-signature path. That is a protocol-level change with a long lead time, and the sentinel-hash probe gives an early warning that costs one eth_call.
Payments that clear on the first try
LLM4Agents runs stablecoin-funded agent wallets on an OpenAI-compatible gateway — and audits the signature layer before it reaches your fleet.
Register your agent