Smart Sessions vs x402: where ERC-7579 session keys meet the payment
Smart Sessions gives a smart account spending caps, usage limits and time frames. That is exactly what an autonomous agent needs before you hand it a wallet. But it enforces those rules on ERC-4337 userOps, and an x402 payment never becomes one.
The permission problem for paying agents has a familiar shape. You want to give an agent a key that can spend, but only so much, only on certain things, only for a while. On EVM smart accounts the standard answer today is Smart Sessions, a session-key module built jointly by Rhinestone and Biconomy for ERC-7579 accounts. It is one of the most widely deployed permission modules in the account-abstraction ecosystem.
We have written before about the Coinbase Spend Permissions path and about Zodiac Roles on Safe. Smart Sessions is the third leg of that stool, and the one closest to the ERC-4337 mainstream. So we read the deployed bytecode, the policy contracts, and the three audits. The module is careful and well-reviewed. But when you point it at x402, the boundary between the two protocols is thinner than it looks, and it runs through the one place session keys are hardest to reason about: the signature.
What Smart Sessions actually is
Smart Sessions is an ERC-7579 validator module. You install it on a modular smart account, then create sessions: each session pairs a session key (any ISessionValidator, from a plain ECDSA signer to a passkey) with a set of policies that constrain what that key may do. The authors are Filipp Makarov of Biconomy and zeroknots.eth of Rhinestone; the license is AGPL-3.0.
The canonical module is deployed at the same address on every major chain. We checked the bytecode ourselves:
// SmartSession module, identical code on Base, Ethereum, Optimism, Arbitrum
const SMART_SESSIONS = '0x00000000008bDABA73cD9815d79069c247Eb4bDA';
// cast codesize on each chain returns the same 23,608 bytes
$ cast codesize 0x00000000008bDABA73cD9815d79069c247Eb4bDA --rpc-url https://mainnet.base.org
// 23608
The policies are separate singletons, each also deployed at a vanity address. On Base the ERC20SpendingLimitPolicy lives at 0x00000088D48cF102A8Cdb0137A9b173f957c6343 (3,116 bytes), the UniversalActionPolicy at 0x0000006DDA6c463511C4e9B05CFc34C1247fCF1F, and the SudoPolicy at 0x0000003111cD8e92337C100F22B7A9dbf8DEE301. A session's Session struct carries three buckets of policy: userOpPolicies (checked against the whole userOp), actions (per target-plus-selector ActionData), and erc7739Policies (the ERC-1271 signature path, more on that below).
For an agent budget the shortlist is short. ERC20SpendingLimitPolicy caps how many tokens the session can move. UsageLimitPolicy caps how many operations it can perform. TimeFramePolicy bounds the validity window. ValueLimitPolicy caps native value. On paper this is the perfect budget layer for an agent that pays per call.
The spending-limit policy does not speak EIP-3009
Here is the first crack. The ERC20SpendingLimitPolicy is an action policy: it inspects the calldata of a token call and decrements a remaining balance. We read its _isTokenTransferOrApprove helper. It branches on exactly four function selectors:
// selectors ERC20SpendingLimitPolicy recognizes
transfer(address,uint256) // 0xa9059cbb
transferFrom(address,address,uint256) // 0x23b872dd
approve(address,uint256) // 0x095ea7b3
increaseAllowance(address,uint256) // 0x39509351
// anything else returns (isTokenTransfer=false) -> VALIDATION_FAILED
x402 does not use any of those. The EIP-3009 settlement path that x402 rides on calls transferWithAuthorization (selector 0xe3ee160e) or receiveWithAuthorization (0xef55bec6). Neither is in the list. If you somehow routed an x402 settlement through the account as an action, the spending-limit policy would see an unrecognized selector, return that it is not a token transfer, and fail validation. The budget layer cannot meter x402 spend, because it does not recognize the shape of an x402 payment.
That is a symptom, not the disease. The deeper point is that x402 settlement is not something the account executes at all.
x402 never becomes a userOp
Walk through what happens when an agent pays for a metered API call under x402. The agent signs an EIP-3009 authorization off-chain: a typed-data message that says "this signer authorizes moving N USDC to the resource server, valid between these timestamps, with this nonce." The agent hands that signature to the resource server or its facilitator. The facilitator then submits transferWithAuthorization to the USDC contract as an ordinary transaction, paying its own gas, and USDC moves the money.
Notice who does what. The account never broadcasts a transaction. There is no userOp. The EntryPoint is not involved. The facilitator's transaction is a call from the facilitator to USDC, and USDC pulls from the signer using the signature. Every Smart Sessions policy that operates on a userOp — the gas policy, the usage-limit policy, the ERC20 spending policy in its userOp guise — is simply never invoked, because there is no userOp to validate.
So Smart Sessions, the execution gatekeeper, does not gate x402 execution. The only surface where the two protocols touch is the signature. And that surface is governed by ERC-1271, which for Smart Sessions means ERC-7739.
The signature boundary: ERC-1271 and ERC-7739
When the paying agent is a smart account rather than an EOA, the EIP-3009 authorization cannot be a raw ECDSA signature — a contract has no private key. USDC handles this. We read Circle's EIP3009.sol: its _requireValidSignature uses SignatureChecker.isValidSignatureNow, which falls back to ERC-1271 when the signer is a contract:
// Circle FiatToken EIP3009._requireValidSignature
SignatureChecker.isValidSignatureNow(
signer,
MessageHashUtils.toTypedDataHash(_domainSeparator(), dataHash),
signature
);
So for a smart-account agent, USDC ends up calling account.isValidSignature(digest, signature), where digest is the plain EIP-3009 TransferWithAuthorization typed-data hash built with USDC's own domain separator. If the account routes ERC-1271 to Smart Sessions, the call lands in isValidSignatureWithSender. And that function does not expect a plain signature.
Smart Sessions implements ERC-7739, the nested-EIP-712 scheme that prevents a signature made for one account from being replayed against another account that shares the same signer. It re-wraps the incoming hash inside a TypedDataSign struct that binds the account's own domain, then checks the signature against that. The wire format the module expects is not r‖s‖v. It is:
// what SmartSession.isValidSignatureWithSender expects
permissionId (32 bytes)
‖ signatureForSessionValidator
‖ APP_DOMAIN_SEPARATOR
‖ contents // the original struct hash
‖ contentsType // e.g. "TransferWithAuthorization(address from,...)"
‖ uint16(contentsType.length)
The module also gates on two things beyond the cryptography. It checks that the permissionId is an enabled session, and that the content name — the type of the thing being signed — is in that session's allowedERC7739Content list. Only the TypedDataSign workflow is supported; the PersonalSign path was removed and now returns false. And per an audit fix we discuss below, at least one ERC-1271 policy must be enabled for the check to pass at all.
Stack that up against how x402 clients actually sign. The buyer SDKs target EOAs: they build the EIP-3009 typed data and call viem's signTypedData, producing a plain 65-byte signature over USDC's own digest. That signature, dropped into the Smart Sessions envelope, has none of the ERC-7739 framing. The module's assembly would read garbage in the last two bytes as the contentsType length, fail to reconstruct a matching hash, and return the failure magic value. USDC would then reject the payment.
TransferWithAuthorization in its allowed content.
This is not a bug in either protocol. ERC-7739 is doing exactly its job — and its job matters enormously for agents, which is the whole point of the next section. But it means a smart-account agent cannot pay x402 through Smart Sessions with an off-the-shelf x402 client. Someone has to build the ERC-7739 wrapping for the TransferWithAuthorization content on the signing side, and provision that content type on the session at enable time.
Why the replay protection is not optional for agents
You might be tempted to skip ERC-7739 and route x402 through a simpler ERC-1271 validator that just checks the raw digest. The audit history says do not. In October 2024, Cantina flagged a High-severity finding in exactly this code: the ERC-7739 logic used the SmartSession module's own address as the verifyingContract in the nested struct, instead of the smart account's address.
"This approach defeats the main purpose of using ERC-7739, as it causes all accounts to modify their hashes in the same predictable way. As a result, two smart accounts that share a signer could be vulnerable to signature replay." — Cantina, Smartsessions Core review, finding 3.1.4
Read that in the agent context. Fleets of agents are the obvious deployment: one operator, one session-signing key, many smart accounts. That is precisely the "multiple accounts share a signer" case ERC-7739 exists to protect. Had that bug shipped, a signature an agent produced to pay from account A could be replayed to pull the same authorization from account B. It was fixed (PRs 64 and 77) so that the account's address binds the hash. But it is a reminder that the replay wrapper is load-bearing for anyone running more than one agent off a shared key — which is to say, everyone running agents at scale.
The same review found a related edge: _erc1271IsValidSignatureNowCalldata originally returned false when a session had zero ERC-1271 policies, and the fix (PR #130) made the design explicit — at least one ERC-1271 policy must be installed for signature validation to pass. If you provision a session for x402 signing and forget the 1271 policy, every payment signature silently fails. That is a configuration cliff worth knowing before it costs an agent an afternoon of "why is USDC rejecting me."
A budget that caps the wrong axis
One more finding lands squarely on agent economics. In July 2025, ChainLight audited the policies and rated Medium a gap in SimpleGasPolicy: it capped the gas limit of a userOp but not the gas price.
"The SimpleGasPolicy only limits the gas limit and does not restrict the gas price, allowing sessions to spend more ETH than the user intended." — ChainLight, Rhinestone Smart Sessions Security Audit, SmartSessions-001
A gas budget that bounds computational steps but not price is not a spend cap — a session could burn far more ETH than intended at a high gas price. It was patched to multiply price by limit. The lesson generalizes past this one policy: for an autonomous agent, the axis that matters is money out, denominated in the unit the agent actually spends. A policy that caps units of computation, or number of calls, or token count, is only a proxy for the dollar figure the operator cares about. Smart Sessions gives you several proxies; none of them is "no more than $50 of USDC this week" unless you compose them carefully — and, as we saw, none of them sees x402 spend at all.
What it means for LLM4Agents
LLM4Agents settles per-call inference in stablecoins over x402 and EIP-3009. A large and growing share of the agents that will call the gateway are, or will be, ERC-7579 smart accounts — that is where the account-abstraction ecosystem is standardizing, and Smart Sessions is its default session-key module. So this boundary is directly on our path.
The blunt conclusion: Smart Sessions does not natively gate an x402 payment. Its execution policies never fire, because x402 settlement is a facilitator transaction, not a userOp. Its spending-limit policy does not recognize the EIP-3009 selectors. And its one relevant surface — ERC-1271 signature validation — only accepts an ERC-7739-wrapped signature over a pre-registered content type, which no stock x402 client produces. A smart-account agent that "has Smart Sessions installed" is not, by that fact, spend-limited on the gateway.
That is a threat if we assume otherwise, and an opportunity if we build for it. It means our gateway should never treat "the agent uses a smart account with a session module" as evidence of a spend cap. The cap that protects LLM4Agents revenue and the agent's treasury is the one enforced on the payment itself — the x402 maxAmountRequired, the EIP-3009 validBefore window, the per-agent balance on our side — not the one an upstream account module may or may not apply. This mirrors what we found auditing ERC-7710 delegation: the on-chain permission layer and the payment layer are different rails, and confusing them leaves a gap.
Staying on the frontier
Concrete steps, in order.
Publish an ERC-7739 signing recipe for x402
The missing piece is client-side. Ship a small helper in our buyer SDK that, when the payer is an ERC-7579 smart account with Smart Sessions, wraps the EIP-3009 TransferWithAuthorization typed data in the ERC-7739 TypedDataSign envelope with the session's permissionId prepended and USDC's domain separator as the APP_DOMAIN_SEPARATOR. Document that the session must be enabled with TransferWithAuthorization(...) in allowedERC7739Content and at least one ERC-1271 policy installed. This is the difference between "smart-account agents can pay us" and "they get a rejection they cannot debug."
Ship an x402-aware spending policy
The ERC20SpendingLimitPolicy gap is fixable at the source: an action policy that recognizes transferWithAuthorization and receiveWithAuthorization, decodes the authorized amount, and meters it against a per-session cap. It would only help for the case where an account executes settlement itself, but it closes the "budget cannot see x402" hole and is a clean upstream contribution to a widely used module.
Never infer a cap from an account module
Make it a hard rule in our billing logic: the authoritative spend cap is the one we enforce on the payment, not one we assume from the payer's wallet architecture. Keep the per-agent balance, the x402 amount check, and the EIP-3009 validity window as the real ceiling, exactly as in our spend-controls audit.
Track the intent-based successor
Rhinestone pushed smart-sessions-v2 (SmartSessionEmissary) on 2026-09-27, extending session keys to work with Uniswap's The Compact for intent-based cross-chain operations. Intents are where the account-abstraction world is heading, and cross-chain settlement is squarely in our lane. Follow that repo; the signing and policy surface it introduces will define the next version of this boundary.
Smart Sessions is a strong, well-audited permission layer. It is not, out of the box, an x402 spend limiter — and knowing exactly why is what lets us build the piece that makes the two fit. The agent economy will run on smart accounts. It will pay with off-chain signatures. The work is at the seam between them.
Build agents that pay with real limits
LLM4Agents enforces the spend cap where it counts — on the payment. OpenAI-compatible gateway, x402 and EIP-3009 settlement, per-agent balances.
Register an agent