AgentCap

← Blog

Signed Is Not Spent: x402 Authorization vs Settlement

GetAgentCap team · published 9/27/2026

When an agent pays for an x402 resource it signs a payment, but it does not send one. Someone else broadcasts it later, or never. The time between signing and settling decides what your spend records should count. Mixing up the two is one of the quietest ways agent spend numbers drift away from the chain.

Two events, not one

An x402 payment has two separate events. First, the client (your agent) signs a payment payload and sends it to the resource server in the X-PAYMENT header. Second, a facilitator checks that payload and submits it on-chain, where it either lands in a block or does not. The first event happens inside your agent's process. Only the second moves any USDC.

The protocol keeps these apart on purpose. The resource server can call a facilitator's verify endpoint to confirm the signature, amount and recipient before doing any work. It then calls settle to broadcast. Once settlement succeeds, the server sends the transaction reference back to the client in the X-PAYMENT-RESPONSE header. Until that happens, the agent holds only a promise it has signed.

For spend tracking the rule is simple: a signature is intent, and a confirmed transfer is spend. A system that counts signatures, HTTP 200s or agent-side logs as spend is counting the wrong event. On a busy fleet the two counts will not match.

What an EIP-3009 authorization actually contains

On Base and other EVM chains, the x402 'exact' scheme uses EIP-3009, which USDC implements natively. The agent signs an EIP-712 typed message for transferWithAuthorization. The message holds from, to, value, validAfter, validBefore and a 32-byte nonce. The agent never touches gas and never broadcasts anything. The facilitator calls transferWithAuthorization on the USDC contract with that signature and pays the gas itself.

The two time fields set a validity window. Before validAfter the contract rejects the authorization. After validBefore it also rejects it, permanently. Between the two, anyone holding the signature can submit it, but only once. The nonce is random, not sequential, so an agent can have many authorizations outstanding at the same moment with no ordering between them.

When the call succeeds, the contract marks the nonce as used and emits AuthorizationUsed(authorizer, nonce) alongside the standard Transfer event. The contract's authorizationState(authorizer, nonce) function tells you whether a given nonce has been consumed. That makes it the one source of truth for whether a specific signed payment became spend.

The Solana equivalent: partially signed transactions and blockhash expiry

On Solana the x402 exact scheme works differently. There is no EIP-3009. Instead the client builds a USDC SPL token transfer transaction, signs it as the token owner, and leaves the fee-payer signature for the facilitator to add. The facilitator co-signs as fee payer and submits. As on Base, the agent pays no network fee directly.

The validity window here is implicit. Every Solana transaction references a recent blockhash, and the network rejects it once that blockhash has aged out. That takes roughly 150 blocks, which is on the order of a minute or two. A partially signed payment the facilitator never submits in time simply dies. Nothing happens on-chain and there is nothing to cancel.

So the Solana window is short and set by the chain, while the EIP-3009 window is whatever the signer chose. Either way, a tracker has to treat an unsettled signature as provisional. It becomes spend only when a finalized transaction shows the USDC moving out of the agent's address.

Where signed-but-unsettled payments come from

Several ordinary conditions leave a signature that never settles. The resource server may verify the payment, then fail before calling settle, for example on a timeout, crash or upstream error. The facilitator may be unavailable or refuse to submit. The on-chain call may revert because the agent's USDC balance was spent by another payment in the meantime. On Solana, the blockhash may simply expire first.

The opposite also happens. An agent may time out waiting for a response, decide the payment failed, and log it as failed, while the facilitator does settle it seconds later. The agent then signs a new authorization for the retry with a fresh nonce, and both settle. The agent's log shows one failure and one success. The chain shows two transfers.

That is why agent-side logs are unreliable both ways. They can over-count, by including signatures that never settled. They can also under-count, by marking settled payments as failed. Only the settlement layer resolves both cases, so that is the layer a spend record should be built on.

Long validity windows are outstanding liabilities

EIP-3009 lets the signer pick validBefore, and some client code sets it generously so slow facilitators never cause rejections. The side effect is that every signed authorization is live until it expires. It counts against the agent's balance, even though no ledger shows it as spent. A signature leaked in a log, a queue or a cached HTTP request can be submitted by anyone until validBefore passes.

EIP-3009 provides cancelAuthorization, which the authorizer can call to burn a nonce before it is used. It is an on-chain transaction and costs gas, so agents rarely call it on routine failures. The practical fix is to keep validBefore short, measured in seconds or minutes rather than hours. That keeps the window of provisional liability close to how long a facilitator actually takes.

For budgeting, the consequence is concrete. A budget that subtracts only settled spend can be overrun by authorizations that are already signed and still valid. A budget that subtracts signatures will overstate spend whenever payments fail. An accurate picture keeps both numbers: settled spend as the actual figure, and live unexpired authorizations as exposure.

How to record it correctly

Key each payment on the chain's own identifiers. On Base, use the authorizer address plus the EIP-3009 nonce, joined to the transaction hash of the Transfer. On Solana, use the transaction signature. Match agent-side records to these, not the reverse. When an agent log entry has no settled counterpart after its validity window closes, it can be safely marked as not spent.

Read settlement from finalized chain data, not from the X-PAYMENT-RESPONSE header alone. The header is a convenient hint that a transaction exists. The block is the proof. Watching USDC Transfer events out of each agent address catches everything that settled, including payments the agent believed had failed and payments settled by a facilitator it did not expect.

Watching outbound transfers by address has a useful property: it needs no cooperation from the agent, the resource server or the facilitator. Whatever path a signature took, if it settled, it shows up as a Transfer from the agent's public address. If it did not, nothing shows up, and the spend is zero.

Settled spend, from a public address

GetAgentCap tracks agent spend at the settlement layer. You add an agent's public Base or Solana address, and we record the USDC transfers that actually left it, x402 payments included. We never ask for keys, signatures or access to the agent's runtime. If you want spend numbers that match the chain instead of your agents' logs, start at getagentcap.com.

This approach can see settled transfers, not live signatures. Unsettled authorizations exist only where they were signed and wherever they were sent. That is why short validBefore windows are worth enforcing in client code no matter which tracker you use.

The short version: signing an x402 payment creates a claim on the agent's balance, and settling it spends the money. Count the second, limit how long the first can last, and your spend records will match the chain.