Preventing Autonomous Agent Double-Purchasing


SAAX Protocol — Solvent Applied Autonomous Exchange Protocol

October 8, 2026


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 bank 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 in the merchant's API 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 combination is a race condition. Retry logic 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. The standard remedy for non-idempotent APIs. The client sends a unique key with the request; the server treats duplicate keys as the same request. This works when the client is the one generating the key. It doesn't work when the client is an agent that may not have a stable identity for "the same intent" across retries.

Nonce-based authorization. Payment protocols like EIP-3009 require a unique nonce per authorization. The chain rejects a second settlement with the same nonce. This prevents double-settlement. It does not prevent double-order — the merchant may have already accepted both orders before either authorization reaches the chain.

Spend limits. A cap on total spend per agent per period. This bounds the damage but doesn't prevent the double-purchase itself. It also fails when the double-purchase is within the limit.

Tool-call deduplication. The agent runtime refuses to execute the same tool call twice within a short window. This works when the runtime has a stable view of what "the same tool call" means. It fails when the retry happens across process boundaries, session restarts, 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

The missing piece is a commitment identifier that is generated before the purchase attempt and 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 SAAX handles this

SAAX Protocol treats the commitment as the primary artifact of the purchase. The commitment is created before execution begins, and it carries an identifier that survives retries, session restarts, and process boundaries.

A SAAX commitment includes:

  • A unique commitment ID, generated at commitment time
  • The authority reference (who authorized this commitment and under what scope)
  • The terms (what the commitment obligates the parties to)
  • The evidence requirements (what counts as fulfillment)
  • The recovery policy (what happens if fulfillment fails)

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 commitment schema, the lifecycle states, and the evidence interface are documented at saax-protocol.com/spec. The conformance suite is at saax-protocol.com/conformance.