2026-08-19
Your agent will retry. Your payment layer had better be ready for it.
Every agent loop I've ever read has a retry in it somewhere, and most of them retry the whole tool call. The model asks for a transfer, the request times out at 29 seconds, the framework calls the tool again, and now you've paid the translator twice for the same 340 words. Nobody wrote a bug, the system just did what systems do when a network is involved.
This is the thing about money in autonomous systems that took me a while to fully respect. A human clicking "pay" twice is a UX problem you can solve with a spinner. An agent calling pay twice is a certainty, indeed it's the default behavior of every retry policy, and the only fix that actually holds is making the second call harmless.
What idempotent means when there's a ledger underneath
The word gets thrown around a lot so let me be concrete about what it means in txn.dev. Every transfer takes an idempotency_key, a string you choose, and the ledger has a unique index on it. When a transfer comes in with a key we've already seen, we don't evaluate anything, we don't touch balances, we just return the transaction that was created the first time. Same id, same amount, same timestamp. The caller can't tell the difference between the first response and the tenth, which is the whole point.
The subtle part is that the check has to happen inside the same database transaction as the debit. If you look up the key, don't find it, and then in a separate step debit the wallet, two concurrent retries can both pass the lookup. So the lookup and the write are one atomic unit, and the unique index is the final backstop if something slips past anyway. Belt, braces, and a database that refuses to let you be wrong.
Jobs work the same way. Posting a job escrows money out of a wallet, so a retried post would hold the budget twice. The job endpoint takes the same key and a replayed post returns the original job with a replayed: true flag, and a 200 rather than a 201 so an agent that cares can tell.
Choose the key like you mean it
The key should identify the intent, not the attempt. If your agent is paying for step 7 of run 42, the key is something like run-42-step-7. If you generate a fresh UUID on every call you've built a very elaborate way of retrying with no protection at all, which I have seen people do, tho usually only once.
The other mistake is reusing a key across genuinely different payments because it was convenient. The ledger will happily return the first transaction and your second payment silently never happens. Same amount, same wallet, different work, different key.
Why this isn't optional
Stripe has had idempotency keys for over a decade and most integrations never set them, because a person is usually in the loop to notice a double charge. There's no person in the loop here. The agent that retried has already moved on to the next step and reported success. The only thing that catches a duplicate payment in an autonomous pipeline is a payment layer that refuses to make one.
So it's in the API contract rather than a best practice in the docs. Every mutating endpoint accepts a key, the SDK surfaces it as a first-class parameter, and the MCP tools pass it through. You can leave it off for a one-off from a REPL, but anything running in a loop should treat it the way you treat a primary key... something you'd be embarrassed to forget.