Auditing Spend Permissions: the onchain budget for agent wallets
Our audit of x402's spend controls ended on a gap: the client-side cap is per payment only, with no accumulated counter or session budget anywhere in the protocol. This week we audited the layer that does keep a counter — onchain, per period, enforced by consensus rather than by an SDK default.
That layer is Coinbase's SpendPermissionManager, the contract behind the "Spend Permissions" feature of what is now branded Base Account (formerly Coinbase Smart Wallet). It is the closest thing in production to the missing piece we flagged in the spend-controls audit: a budget that survives across payments because the chain itself does the accounting. As in every audit in this series, we worked from primary sources: we cloned the repo, read all 1,078 lines of Solidity, verified the deployed bytecode on every chain the README claims, and counted every event the contract emitted over the last seven days.
The object: nine fields, one signature
A spend permission is an EIP-712 struct, signed once by the user's smart account. The whole model fits in nine fields:
struct SpendPermission {
address account; // smart account whose tokens can be spent
address spender; // entity allowed to spend them
address token; // ERC-7528 native token or ERC-20
uint160 allowance; // max spend per period
uint48 period; // reset interval, in seconds
uint48 start; // valid from (inclusive)
uint48 end; // valid until (exclusive)
uint256 salt; // differentiates otherwise-identical permissions
bytes extraData; // opaque payload for the spender
}
The semantics are "10 USDC per month to this spender until this date". Not a call scope, not a session key with arbitrary powers — a token, an amount, a clock. The README is explicit that this narrowness is the point: the design "does not enable apps to make arbitrary external calls from user accounts", and ERC-721 tokens are actively rejected at approval time (ERC721TokenNotSupported). The EIP-712 domain is "Spend Permission Manager", version 1, and approvals accept ERC-6492 signatures, so a permission can be signed before the smart account is even deployed — the validator deploys it as a side effect the first time the permission is used.
The accounting: a counter the client can't forget
Everything our x402 audit found missing client-side happens here in about thirty lines. Each permission hashes to a storage slot holding a PeriodSpend struct: period start, period end, accumulated spend. On every spend() call, _useSpendPermission loads the current period, adds the new value, and reverts with ExceededSpendPermission(value, allowance) if the total crosses the allowance. When the clock passes a period boundary, the counter resets to zero and the same allowance is available again.
Two properties of that design matter for agent operators. First, the periods are a fixed schedule, not a rolling window: boundaries sit at start + n * period, deterministically, forever. The accounting doc spells this out with worked examples. The consequence is that a spender can drain the full allowance in the last block of period n and again in the first block of period n+1 — up to 2x the per-period budget in a few seconds, once per boundary. That is by design and cheap to reason about, but it is not a rate limit.
Second, there is no per-transaction cap at all. The allowance bounds the period, and a single call can consume all of it. The two layers are exact mirrors: x402's client-side spend controls cap each payment and forget the past; the onchain permission remembers the past and doesn't care about any single payment. Neither references the other.
Revocation is symmetric and immediate: the account can revoke(), and the spender can revokeAsSpender() to walk away from its own permission. There is also a subtle anti-frontrunning device we didn't expect to find: approveWithRevoke() atomically replaces one permission with another, but only if the revoked permission's accounting state matches an expectedLastUpdatedPeriod the account passes in — so a spender can't race the replacement with one last drain.
The wallet coupling: an owner, not a module standard
How does a singleton contract move funds out of user wallets? By being an owner of them. Rather than shipping a Smart Wallet V2, Coinbase leveraged V1's modular owner system: the SpendPermissionManager singleton is added as an owner of the user's account, and _execute calls CoinbaseSmartWallet.execute directly. The spend path never touches the ERC-4337 EntryPoint — the README notes this avoids paymasters spending user tokens on gas.
The cost of that shortcut is coupling. This is not an ERC-7715 implementation and not an ERC-7579 module: it works because Coinbase's own wallet accepts it as an owner. The repo knows it: open PRs include a draft ERC7579SpendPermissionManager (#69, open since March 2025) and an "EOA-supported SpendPermissionManager" (#66, February 2025). Neither has merged. ERC-7715 itself — the interoperable way for an app to request permissions from any wallet, with wallet_requestExecutionPermissions — has sat in Draft status since May 2024. The interoperable standard is a draft; the proprietary-but-open singleton is what's deployed and audited.
Details only the source shows
Three mechanisms in the 776 lines of SpendPermissionManager.sol are worth knowing even if you never read it. The first is how funds actually move. spend() transfers tokens from the account to the spender — not to a merchant. For ERC-20s, the manager instructs the account to approve the exact value to the manager itself, then calls safeTransferFrom, so no standing token allowance survives the call. For native tokens it is stranger: the account sends ETH to the manager, guarded by a transient-storage variable holding the exact expected amount — the receive() function reverts on any other value at any other time — and the manager forwards it to the spender. Tight, but it means every spend is two hops, and the spender is always the custodian of freshly pulled funds until it does something with them.
The second is the MagicSpend path. spendWithWithdraw() lets a spender atomically fund the account from Coinbase's MagicSpend singleton and spend in the same transaction — just-in-time liquidity for accounts that don't hold the token. The binding is careful: the withdraw request's asset must match the permission's token, the amount can't exceed the spend value, and the lower 128 bits of the withdraw nonce must equal the lower 128 bits of the permission hash, so a withdraw signature can't be replayed against a different permission.
The third is batching. approveBatchWithSignature approves many permissions — same account, same period window, different spenders, tokens, and allowances — under one EIP-712 signature. For agent operators this is the fleet primitive: one human signature provisions per-agent, per-token budgets for an entire roster. The batch hash construction (SpendPermissionBatch wrapping PermissionDetails[]) is all onchain-verifiable, and each permission in the batch remains individually revocable afterwards.
What we verified onchain
The README claims one address — 0xf85210B21cC50302F477BA56686d2019dC9b67Ad — on eight mainnets: Base, Ethereum, Optimism, Arbitrum, Polygon, Avalanche, BNB Smart Chain, and Zora. We called eth_getCode on all eight on 2026-08-25. All eight return the same 12,610-byte runtime code with the same two immutables embedded: the PublicERC6492Validator at 0xcfCE48…293D and the MagicSpend singleton at 0x011A61…aB92. Byte-diffing the Base and Ethereum deployments, exactly 34 bytes differ, in two regions: the 32-byte cached EIP-712 domain separator and the 2-byte cached chain id (0x2105 vs 0x01). Everything else is byte-identical.
Then we counted usage. Scanning the 302,400 blocks ending at block 50,429,665 on Base — seven days, roughly August 18 to 25 — the contract emitted 547 SpendPermissionUsed events, 368 SpendPermissionApproved, and 13 SpendPermissionRevoked, across 351 distinct permission hashes, 231 distinct accounts, and 331 distinct spender addresses. USDC dominates: 492 of the 547 spends, totaling 15,236.45 USDC — an average of about $31 per spend. The remainder is 55 spends of a long-tail token called BEATS. Note the ratio of spenders to permissions: 331 spenders across 351 permissions means spender addresses are essentially single-purpose — one fresh address per granted permission, which is exactly what agent- or session-scoped spenders look like.
The other chains are quieter. Full seven-day scans of Optimism, Avalanche, and Zora returned zero events, as did a sample of Arbitrum's most recent 100,000 blocks. Ethereum, Polygon, and BSC we could not scan — every public RPC endpoint we tried refuses eth_getLogs at that range — so we report them as unmeasured rather than zero. But the shape is clear: this is a Base-native system with a multichain footprint waiting for demand.
bde6fe5, 2026-08-25), the manager's address appears in exactly five files — all generated browser-paywall template bundles that embed the Base Account SDK for the human checkout path. The string SpendPermissionManager appears zero times. No scheme, client, or facilitator in the agent-side protocol path knows the onchain budget exists.
The agent seams that do exist
Coinbase's own agent stack, by contrast, is already wired in. The CDP docs list "Agentic payments — control your agent's spending limits for autonomous operations" as a first-class use case, alongside subscriptions and algorithmic trading, with server-side createSpendPermission / listSpendPermissions / useSpendPermission / revokeSpendPermission methods. (Curiously, those docs list six mainnets, omitting Zora and BSC from the README's eight.)
And AgentKit — Coinbase's framework for giving LLMs wallets — exposes the whole loop as tools the model can call: list_spend_permissions and use_spend_permission on CDP wallets, plus list_base_account_spend_permissions, spend_from_base_account_permission, and revoke_base_account_spend_permission for Base Accounts. The implementation has choices worth knowing before you hand them to a model: use_spend_permission "automatically finds the latest valid spend permission" rather than taking an explicit one, and the Base Account spend tool's amount is optional — "if not provided, will withdraw the full remaining allowance." A drain-by-default tool parameter is exactly the kind of thing the onchain allowance exists to make survivable; it is still worth noticing that the default is drain.
Distribution is not the bottleneck either. The npm package carrying the client SDK, @base-org/account, pulled 6.70 million downloads in the 30 days ending August 23; @coinbase/cdp-sdk pulled 3.41 million; @coinbase/agentkit, 42,908. The rails are shipped and installed. The 547 weekly spends say actual autonomous usage is early — hundreds of agents, not millions.
A repo that shipped and stopped
The repo history tells a compressed story. Created October 2024 (first commit June 2024), 415 commits, MIT license, three Cantina audits in three months (October, November, December 2024) — then the manager was done. HEAD as of this audit is from 2026-03-24. The 2026 activity is all about a second contract, SpendRouter: a 274-line singleton that decodes an (executor, recipient) pair from the permission's extraData and forwards spent funds directly to a merchant, closing a gap in the base design where spend() can only move funds to the spender itself. SpendRouter got two Cantina audits in March 2026 (the 18th and 21st) — and the README still lists its deployment address as "TBD" five months later. Audited, unreleased.
Meanwhile 13 items sit open — 3 issues, 10 PRs — including the interoperability work (ERC-7579, EOA support, Permit3 explorations) and an issue proposing composition with ERC-8265 prepared-transaction envelopes. The pattern rhymes with what we found in the ERC-8004 contracts audit: the core is small, audited, and live; the ecosystem edges are where momentum goes to wait.
What it means for LLM4Agents
For a gateway whose agents pay per call in stablecoins, spend permissions solve the problem that sits one layer above x402: not "how does an agent pay for this request" but "how much can this agent spend before a human looks again". Today LLM4Agents answers that with deposited balances — the agent can't spend what wasn't deposited. A spend permission inverts the flow: the operator's funds stay in their own account, and the agent (or the gateway acting as spender) pulls up to N USDC per period as usage accrues. That is a materially better treasury story for operators running fleets: no idle deposits spread across providers, one revocable permission per agent, and a consensus-enforced monthly cap that no compromised SDK config can lift — the exact failure mode we demonstrated in the spend-controls audit, where flipping allowedAssets silently removed the ceiling.
The threat side is equally concrete. Base Account plus CDP wallets plus AgentKit plus the x402 facilitator is a vertically integrated loop: Coinbase holds the wallet, grants the budget, executes the spend, and settles the payment. A gateway that only accepts pre-funded deposits looks rigid next to "grant your agent $50/month, revoke anytime". And the measured numbers — 547 spends a week, $31 average — say the market for onchain agent budgets is small enough that whoever integrates early helps define the conventions.
Staying on the frontier
Concrete steps, in order. First, accept spend permissions as a funding rail: let an operator grant LLM4Agents' treasury address a periodic USDC allowance on Base and have the gateway pull deposits as balances run down, with getCurrentPeriod surfaced in the dashboard so the remaining onchain budget is visible next to the gateway's own metering. The contract is a singleton at a known address on eight chains; the integration surface is three functions.
Second, compose the two caps explicitly in agent policy: per-payment ceilings from x402 spend controls, per-period ceilings from the onchain allowance, and gateway-side accumulation between them — three layers, three different failure domains. Document it as the reference budget architecture for autonomous agents, because nobody else has: the two layers ship from the same company and currently never mention each other.
Third, watch two artifacts. SpendRouter's deployment would make direct merchant settlement from user accounts practical — relevant to whether future x402 schemes can settle from a permission instead of an agent hot wallet. And ERC-7715 leaving Draft would make permission requests wallet-agnostic, which is when a gateway should support granting budgets from non-Coinbase wallets. Neither requires building today; both deserve a tripwire.
The one-line summary of this audit: the agent economy's budget enforcement is now split between an SDK default that forgets and a contract that remembers — and the protocol seam between them is still unsewn. The layer that sews it will own a very useful piece of the stack.
Give your agents a budget, not a blank check
LLM4Agents meters every call against balances you control — 345+ models behind one OpenAI-compatible gateway.
Register your agent