When an AI agent retries a paid API call: how we stop it from paying twice
An AI agent calls a paid API, the payment goes through, and the response never arrives. The connection dropped, a proxy timed out, or the agent's own runtime restarted. From the agent's side the call failed, so it does the reasonable thing and tries again. If the API charges per call, that retry is

An AI agent calls a paid API, the payment goes through, and the response never arrives. The connection dropped, a proxy timed out, or the agent's own runtime restarted. From the agent's side the call failed, so it does the reasonable thing and tries again.
If the API charges per call, that retry is where money goes missing. The first payment may have settled on chain while the agent never saw the result. A naive retry pays a second time for the same work.
We run an MCP gateway where agents pay per call in USDC, over x402 on Base or MPP on Tempo. This post walks through the four layers we use so that a retry never turns into a double charge, and where each layer stops helping.
With x402 on Base, the agent does not send money directly. It signs an authorization for a USDC transfer (EIP-3009), and the server submits it. Each authorization carries a nonce, and the token contract accepts a given nonce from a given payer only once.
So if the agent resends the exact same signed payment, the second transfer fails on chain. No server-side bookkeeping is needed for that guarantee, and it holds even if our own records are lost. A new charge always needs a new signature from the agent.
MPP has the same property on its side. A credential that was already used comes back as 403 credential_replayed, and the call is not charged.
Layer 1 stops the double charge, but it leaves the agent stuck: it paid, it has no result, and resending the payment now fails. To fix that, on our order payments (agents buying from shops through the gateway) the agent can attach a payment id to the payment: the payment-identifier extension of x402, 16 to 128 characters.
The last rule matters more than it looks. If a changed request with an old id quietly got the old result, idempotency would hide a client bug. The agent would think it bought one thing and actually receive another. A conflict forces the client to notice.
The hardest case is the one in the middle. We broadcast the settlement, and then the facilitator call times out or the transaction is still pending. Did the payment happen?
We treat that state as unknown and never as failed. A background job reads the chain: the authorization state for that payer and nonce, and the USDC transfer itself. If it confirms, the order becomes paid and the result is delivered. If it is still not confirmed after 24 hours, the order is marked failed for good and any reserved stock is released.
What we never do is settle the same authorization a second time to "make sure". That is the classic way a timeout turns into two charges.
For tool calls, we only charge for a delivered response. If the upstream provider rejects the input or fails, the call is not charged. Last week that rule covered 1,905 calls rejected by providers, none of them billed.
This does not prevent retries, but it means a retry after a genuine failure is a fresh, fair purchase rather than a second payment for a call that never produced anything.
The 24-hour window is a choice, not a law. An agent that comes back after 25 hours with the same payment id will not get the stored answer. It still cannot be charged twice, because Layer 1 rejects the old signature on chain. What it loses is the cached result, so it has to decide whether to pay again for a fresh call.
That is the trade-off we accepted. The record only needs to cover the agent's realistic retry and recovery period. The guarantee against double charging does not depend on the record at all, because it lives in the payment itself.
None of this is specific to AI. It is distributed-systems hygiene that matters more once the client is a program that retries on its own, at machine speed, with a wallet attached.