Profile
Back to NewsBack
Dev.to 7 min
Reader Mode
An agent payment security checklist, from the payout address to the signer

An agent payment security checklist, from the payout address to the signer

11 hours ago

Yesterday I wrote up five AI agent payment designs I'd looked at in October: an x402 facilitator, a swap API for agents, an x402 test suite, a wallet guard and a banking agent. The payments worked in all five. The gap was always in something the payment path accepted without checking.

The day before that post went out, Binance lost about $2.5 billion from one hot wallet in under four minutes, through an EIP-7702 delegation signed by that wallet's own key.

So this is the full list I'd go through now before an agent is allowed to move money. Some of it repeats that post, most of it is new.

What happened to Binance 14

At 20:20:59 UTC an attacker sent one transaction carrying 50 EIP-7702 authorizations. Each was signed by the wallet Etherscan labels "Binance 14", each for a different consecutive nonce, all pointing the wallet at a sweeper contract. Ethereum used the one matching the wallet's next nonce. At 20:24 the first sweep took about 167,464 ETH and roughly 1.6 billion USDT. The wallet stayed delegated until 00:10, and deposits Binance kept routing into it were swept again at 21:04 and 21:44 (CoinDesk).

Binance says it found no evidence that private keys leaked. How the attacker got the 50 signatures was still under investigation when I checked on 31 October. That part shows up in checks 10, 11, 14 and 15.

1. Where the money goes (4 checks)

  1. The strongest control sets the payout first. In the x402 facilitator, a merchant had to sign to change where their vault pays, but the operator deployed the vault and its first payout came from a dashboard setting. A leaked operator key could create vaults that pay someone else before any merchant signs. If the payout is written once at creation, a fix in the factory only covers new contracts, so go back through the old ones too.
  2. Every contract address is pinned per chain in your config. The swap API added recipient and minimum-output checks, but took the bridge's deposit contract address from the same bridge API it was checking. A compromised API could list its own contract and every check would still pass. Alert when the API and your config disagree.
  3. Calldata gets decoded before anyone signs it. If the agent signs calldata an API returned, that API is part of the agent's wallet. Check the target is on an allowlist, the recipient is the user and the minimum output matches the quote.
  4. A smart wallet address means nothing on another chain. In 2022, 20 million OP sent to Wintermute's address on Optimism went to someone else, because their Safe existed on mainnet but not yet on Optimism and an attacker deployed one there first. Verify ERC-1271 signatures and smart wallet payouts on the chain where the money lands.

2. What a payment is bound to (4 checks)

  1. Each payment is bound to amount, recipient, resource, token contract and network. A server that only checks that a settled payment exists serves whatever route the proof is presented to.
  2. Substitution tests run in CI. A valid payment for a cheaper route, to another recipient, in another token or on a testnet has to fail against your most expensive mainnet route. The x402 test suite added all of these as checks.
  3. One payment buys one response. Mark it as used atomically before the response goes out, or the same signed payment sent 20 times at once buys 20 responses.
  4. Client spend limits pin the token and the network, not only the number. In x402 the 402 response tells the client which token contract and network to pay on, so an endpoint can keep the number the same and change the token.

3. What the agent's key can sign (4 checks)

  1. You have a list of every signature the key can produce. Transactions, EIP-712 typed data, Permit and Permit2, plain messages and EIP-7702 authorizations. The wallet guard held unlimited approvals and Permits by default and still approved a type 4 transaction that delegated the whole account. Its author fixed it within three hours.
  2. Keys that should never delegate refuse the authorization payload. An authorization is a signature over keccak256(0x05 || rlp([chain_id, address, nonce])), with no recipient and no amount in it, and chain_id 0 makes it valid everywhere. A hot wallet has no reason to sign one. A rule that only blocks type 4 transactions sent from your hot wallet wouldn't have fired at Binance, because the attacker sent the transaction and only the signatures came from Binance 14's key.
  3. The signer decodes what it signs. An MPC policy engine or HSM that signs whatever 32-byte hash it's handed can't apply any policy. Have it build the hash itself from the decoded request.
  4. The guard is the only way to the key. A client-side guard only sees what goes through the client it wraps. If the agent or another service can reach the raw key, the guard never sees the signature.

4. Watching the account (3 checks)

  1. The wallet reads its own code before each send. A delegated account's eth_getCode returns 0xef0100 followed by the delegate's address. Stop if it points somewhere you don't expect. The wallet guard shipped this two days after I suggested it, with a list of expected delegates so teams that delegate on purpose don't have to turn it off.
  2. Funds stop flowing into a wallet whose code changed. Whatever watches your hot wallets' balances can call eth_getCode every block too. It wouldn't have stopped Binance 14's first sweep, but it could have stopped deposits going in before 21:04 and 21:44.
  3. The revoke is ready, and so is the nonce math. An authorization to address(0) clears a delegation. But authorizations signed for later nonces stay valid until the account's nonce passes them, and the Binance 14 transaction carried 50 of them. After a revoke, push the nonce past the highest one an attacker could hold, and if the signer itself is compromised, move the funds and retire the key.

5. What the model and the human see (2 checks)

  1. The model writes no free text that leaves your system. Transfer notes, memos, email bodies, metadata. The banking agent kept the payee and amount out of the model's hands, but the model still wrote the transfer note, which lands with someone outside the bank. Let the customer fill it on the confirm screen.
  2. The human approves what gets signed, rendered by code. Token contract, network, amount, recipient and expiry, built from the payload, not the agent's summary, on a screen outside the chat.

6. Operators, tools and logs (3 checks)

  1. Operator keys that can set payouts are treated as treasury keys. Separate, monitored, rotated, and every payout they set raises an alert.
  2. Read-only is enforced by the database role. dbhub's read-only mode (GHSA-mwwr-p57h-56pf) never reached the connection, so a SELECT calling a function with side effects could still write. And a read tool next to a tool that sends anything out can still leak what it reads.
  3. Every payment decision is logged with its inputs. Which requirements, which quote, which upstream answer, which approval, which signature.

Before you ship

  • Draw the path from the agent's decision to the money landing, and mark every value that comes from outside your code.
  • Run the substitution tests from check 6 against your real routes on a testnet.
  • Send your signer one EIP-7702 authorization request and see whether it signs.
  • Write down who can revoke a delegation, rotate a key and stop routing funds, and how long each takes.

Adapted from a checklist our team published on zealynx.io.

I wrote this with help from AI for drafting and editing. The opinions and the review experience are mine.

Chat with me