Preventing double-purchases in agentic commerce — the retry-storm problem
An agent retries a tool call. The tool call was a purchase. The merchant receives two valid orders. The agent's ledger records one transaction. The buyer's statement shows two.
This is the most common failure mode in agentic commerce, and it isn't a bug in any single component. It's the predictable result of three design decisions that were each reasonable on their own.
Agents retry. Reliable agent design requires retry logic. Network calls fail, timeouts happen, models hallucinate transient errors. The standard remedy is to retry until the call succeeds.
Purchases are non-idempotent by default. A POST to create an order creates an order. A second POST creates a second order. Nothing distinguishes "this is a retry of the same intent" from "this is a new purchase."
Settlement is asynchronous. The agent's tool call returns when the order is acknowledged, not when the payment settles. Between acknowledgment and settlement, the agent has no signal that the purchase is already in flight.
The race condition: retry fires before the settlement confirmation arrives. The agent sees no successful response to the first attempt. It retries. The merchant processes both.
Why this is different from consumer double-charges
Consumer double-charges are typically a payment-processor bug — the same authorization submitted twice by accident. The consumer notices on their statement and calls the bank. The card network has a straightforward remedy: reverse the duplicate.
Agent double-purchases are structural, not accidental. The agent was designed to retry. The merchant was designed to accept valid requests. The payment rail was designed to settle what it receives. Everyone behaved correctly within their own boundary. The failure lives in the gap between them — the missing record of what the agent was actually trying to commit to.
The partial fixes that don't fully work
Idempotency keys work when the client generates the key and has a stable identity for "the same intent." Agents often don't.
Nonce-based authorization (EIP-3009) prevents double-settlement. It doesn't prevent double-order — the merchant may have already accepted both orders before either authorization reaches the chain.
Spend limits bound the damage. They don't prevent the double-purchase.
Tool-call deduplication works in-process. It fails across session restarts, process boundaries, or model context resets.
Each of these addresses part of the problem. None addresses the record that makes the duplicate detectable in the first place.
What's actually missing
A commitment identifier generated before the purchase attempt that both the agent and the merchant treat as authoritative.
This is different from an idempotency key. An idempotency key is generated by the client and honored by the server. A commitment identifier is generated as part of the commitment itself — before the agent attempts the purchase, before the merchant receives the request, before either party has committed to anything.
The commitment record binds:
The specific intent (what the agent is trying to buy)
The authority (what the agent is allowed to buy, under what limits)
The terms (price, deliverable, deadline, acceptance criteria)
The unique commitment identifier that ties the retry to the original intent
When the agent retries, the retry carries the same commitment identifier. When the merchant receives the second request, it recognizes it as the same commitment. When the payment rail receives the second authorization, the commitment record prevents the duplicate settlement — because the commitment is the unit of truth, not the individual purchase attempt.
How I'm building this
I've been building a protocol layer for this problem. It's called SAAX Protocol — Solvent Applied Autonomous Exchange Protocol. A vendor-neutral commitment lifecycle for autonomous commerce.
A SAAX commitment includes a unique commitment ID, the authority reference, the terms, the evidence requirements, and the recovery policy. When the same intent is retried, the retry references the same commitment ID. The merchant, the agent, and the settlement rail all work from the same record.
The spec is public at saax-protocol.com/spec. The conformance suite is at saax-protocol.com/conformance. There's a working MCP endpoint at https://www.saax-protocol.com/mcp that handles discovery, routing, and validation — free to test, no signup.
The protocol is vendor-neutral and open for comment. Would be interested in feedback from anyone working on this problem — especially where the spec is thin or unclear.
Curious how others are handling retry-safe agent purchases. What's your approach?