An AI agent sends a supplier payout, and the payment rail times out without answering. The agent has no way to tell if the payment went through, so it tries again with a new request. Both attempts go through, two payments leave the omnibus account for a single invoice, and the idempotency layer never catches the duplicate because it saw two different requests.
Engineers connecting AI agent frameworks to payment systems run into this problem early, and the fallout goes well beyond one bad payment. Simon Taylor, of Fintech Brainfood, makes clear that agentic commerce is already live in narrow, early production, with examples like Shopify's checkout embedded in ChatGPT and Perplexity's Comet browser. Regulators are starting to pay attention to how agent-initiated payments are governed.
When an agent double-pays after an unclear retry, no one can point to which part of the system approved the second payment. Then, the team ends up sorting out both the refund and who owns the mistake.
AI agents can pay twice, but idempotency is what keeps that from happening. An idempotent action has the same result whether you run it once or a hundred times, so a repeated payment request lands as a single ledger entry and doesn't pull money from the omnibus account twice.
This article explains why agents make retries harder, compares at-least-once and exactly-once delivery, explains deterministic keys as the fix, shows how to enforce idempotency at the ledger, and uses an outbox to write the payment intent safely.
AI agents make retries harder because non-deterministic planners, tool-call retries, replayed webhooks, and duplicated cron ticks can each fire independently against the same payment path. Every layer that can decide to retry on its own is another chance for a duplicate payment to land.
A non-deterministic planner treats every unclear outcome as a fresh decision, so two runs of the same task can produce two payment requests with slightly different details. If your duplicate check just compares the raw contents of the request, small differences are enough to make the second request look brand new. The planner might write the payment description a little differently the second time, or list the invoice items in a different order, and that alone is enough to slip past the check. It is the same intent but a different-looking request, so it comes out as two payments.
When an agent framework hits a network error, it retries the call and stamps each attempt with a new ID. If your duplicate check relies on that ID, it stops working the moment the framework generates a new one for a retry. Without an ID tied to the actual business event (like the invoice being paid), the system can't tell a retry apart from a fresh request. Starting a new agent session can also generate a new ID, and nothing links the attempts back to the original.
Providers retry webhooks whenever they don't get a success response, and the agent's reaction to a replayed webhook can kick off the same payment a second time through a different part of the system. The original payment request and the one triggered by the replayed webhook usually run through separate parts of the code, so they don't share the same request contents or the same ID. Without a shared business identifier like an invoice number, nothing is tying the two together.
Scheduled jobs that wake agents can fire twice when the scheduler hiccups or when clocks drift between servers, and the agent has no way to know a second tick has arrived. It plans from scratch, generates fresh parameters, and executes with full confidence. Any safeguard that only looks inside a single run stays blind to a duplicated tick.
Agent payments have two delivery models: at-least-once and exactly-once.
| At-least-once (transport) | At-most-once (ledger effect) | |
| Guarantee | The message will arrive, possibly more than once. | The payment posts to the ledger at most once. |
| What fails | Timeouts, dropped connections, and crashed processes cause the sender to resend without knowing if the original ran. | Without a stable key, replays land as separate postings and money leaves the omnibus account twice. |
| What idempotency does | Nothing at the wire level. Retries continue as normal. | Collapses every replay carrying the same key into a single posting, so the effect stays at-most-once. |
At-least-once means the network will resend a message whenever things are unclear. If a request times out, a connection drops, or a process crashes, the sender can't tell whether the request never ran or whether it ran, but the confirmation got lost, so it sends the request again.
Exactly-once is not a network property. Any system that advertises it is running duplicate detection somewhere downstream, usually at the point of effect.
Payments make the split easy to see. The same instruction might travel across the network many times, but the ledger only needs to record it once. Separating what happens on the wire from what happens in the ledger shifts the engineering work away from trying to make the network perfect and toward catching duplicates at the point of effect.
What you actually want is at-least-once transport paired with at-most-once effect, and idempotency is the mechanism that gets you there. For idempotency to stop duplicates, every replay of the same payment must carry the same key, which means deriving the key from a stable identifier.
Implement the fix by deriving the idempotency key from an immutable event ID using the pattern vendor:event:{id}, so any replay, from any retry source, lands as a single ledger transaction.
The event ID anchors every duplicate check downstream. If a protocol already gives you a stable identifier (a Stripe event ID, an invoice number, or an AP2 intent ID), use that. If it doesn't, assign your own stable ID at the business event and reuse it everywhere.
Apply these four rules to make deterministic keys reliable in production:
Build the key from stable identifiers using the pattern vendor:event:{id}, for example stripe:charge:evt_1P... or internal:invoice:88213. The orchestration layer computes the key, but the model never sees it, so a reworded prompt can't change it. Because the key can be recomputed from the event, it survives agent restarts and fresh sessions.
Refunds and reversals use a parallel namespace such as vendor:reversal:{id}, tied to the original event ID. Without a separate namespace, a refund can match against the payment it is trying to reverse and report success while doing nothing.
Replays carrying the same key resolve to one posting. A request that arrives under an existing key but with a different payload is rejected with a 409 Conflict rather than silently accepted. A bug in key derivation surfaces to engineering instead of becoming a duplicate payment for finance to address.
Keep idempotency keys long enough to outlast the longest realistic retry chain, including webhook replays and human resubmits that can arrive days later. A retention window shorter than the retry horizon brings duplicates back at the tail.
These rules ensure that one business event maps to one payment, and reversals stay distinct from the payments they cancel.
The idempotency key belongs in the ledger, not in the agent or the app. Enforce the rule that one payment intent produces at most one posting in the same step that writes the posting, or idempotency isn't enforced.
Formance is a programmable financial ledger built to enforce idempotency at the ledger boundary so agent-initiated payments resolve to a single posting no matter how many times a retry, replay, or duplicated tick fires the same intent.
Agents are non-deterministic, restart, and don't hold durable state, so an agent that crashes mid-task has no reliable memory of what it already paid.
The application layer can't enforce it reliably either. Multiple app instances run behind caches that expire at different times, gateway records of past requests may drop before a retry arrives, and none of that cache state is tied to the actual ledger write.
In the worst case, the app says the payment is COMPLETED while the ledger holds two. The ledger can enforce the rule because the same write that records the payment also claims the key.
When a replay arrives with the same idempotency key, the ledger returns the original transaction (an Idempotency-Hit: true response) instead of creating a second posting. That means a retry loop, a replayed webhook, or a human clicking submit twice all resolve to the same ledger entry.
Track how often Idempotency-Hit: true fires, and set an alarm for cases where the same key arrives with a different payload, so bugs in how keys are built surface early instead of showing up as duplicate payments.
Claiming the key atomically also handles cases where two identical requests arrive at the same instant, so parallel agents working the same task agree without coordinating.
When an AI agent fires the same supplier payout twice (once on the original run, and once on a retry after a timeout), the ledger has to reject the second attempt without any coordination from the agent.
Numscript, the transaction language available in Formance Ledger, handles this by binding the idempotency key to the posting itself. The same commit that moves the money also claims the key, so a replay under the same key can never produce a second posting.
The transaction below shows what that looks like in practice. A supplier payout of $1,250,000.00 moves from the platform's treasury operating account to Acme's payable account, submitted with an intent-derived idempotency key such as payout:invoice-88213:
// SUPPLIER_PAYOUT
// Event: pay Acme's payable balance through the platform's outbound clearing account
// Idempotency-Key: payout:invoice-88213
send [USD/2 125000000] (
source = @treasury:operating
destination = @suppliers:acme:payable
)
set_tx_meta("event_type", "supplier_payout")
set_tx_meta("intent_id", "invoice-88213")
If the agent retries with the same payout:invoice-88213 key (whether from a framework retry, a replayed webhook, or a duplicated cron tick), the ledger returns the original posting instead of moving another $1,250,000.00. The intent ID stored in transaction metadata also gives auditors a direct link from the ledger entry back to the business event that authorized it.
The transactional outbox stops payments from getting lost between the app and the ledger. The idea is to write the payment intent into the same database transaction as the business change (the invoice getting marked paid), then have a background worker pick it up and send it to the ledger.
Ledger-level idempotency stops duplicates, but it doesn't stop a payment from silently going missing between the app and the ledger. The outbox fills that gap for agentic payments.
The outbox solves the problem of keeping two writes in sync when agents hit it constantly. The agent updates the app (marks the invoice paid), then calls the ledger. Agents crash mid-plan, drop tool calls halfway through, and get killed between steps, so the gap between those two writes is exactly where they fail. If the crash happens in the middle, the app says paid but the ledger has no record. Nothing retries, because nothing knows a retry is needed. Duplicate payments are half the reliability problem for agent payments; a payment that quietly disappears is the other half.
The outbox handles this by writing both the business record and a copy of the payment intent (with its idempotency key) in a single database write. A background worker reads the outbox and sends the payment to the ledger, retrying on failure. Since the ledger rejects duplicates on the key, the worker can retry as often as needed without risking double payment. And because the intent is stored, the agent can crash, restart, or hand off to a new session without losing it.
Once the outbox is in place, four operational details decide whether it holds up in production:
The outbox ensures the agent writes an intent, and that intent reaches the ledger exactly once, no matter what fails in between.
There are five checks to run through before an AI agent payment flow is implemented:
When a duplicate payment dispute lands on someone's desk, the question is always the same: which part of the system approved the second payment?
Formance Ledger records every entry with its idempotency key and the intent that authorized it in one transactional write, so the answer is right there in the ledger. Clone Formance Ledger on GitHub, run it locally, and submit the same transaction twice with one idempotency key.