⚡ Agent Developers 300+ pay-per-call APIs. Give your agent a wallet in 10 seconds. Get 500 Free Credits →
← All posts

August 7, 2026

How to Make Agent Payments Reliable — Receipt Idempotency, Timeout Recovery, and Avoiding Double Charges in x402

A reader of my earlier x402 tutorial asked a set of questions that every engineer building on agent payment protocols eventually hits:

"Bind each payment proof to the request method, path, amount, and a short expiry, then reject a reused receipt. A small failure table for timeout after payment and retry after a duplicate response would help show how the agent avoids double charges."

These aren't edge cases. They're the difference between a payment flow that works in a demo and one you'd trust an autonomous agent to use unsupervised. Let's walk through each problem and the patterns that solve it.

The Core Problem: Agent Payments Are Eventually Consistent

An x402 payment has three phases:

  1. Request — Buyer agent sends HTTP request, gets back 402 Payment Required with payment details
  2. Pay — Buyer submits USDC on-chain (Base L2, ~$0.003 gas)
  3. Deliver — Buyer re-sends request with x402-receipt header, seller verifies on-chain confirmation, returns result

Each phase can fail independently. Phase 2 can succeed on-chain while Phase 3 never completes. Phase 3 can be replayed with the same receipt. The seller can crash after confirming payment but before returning the result. An agent needs to handle all of these without a human watching.

Pattern 1: Receipt Binding — What Should a Receipt Prove?

The x402 receipt standard binds to the request body hash and timestamp. This proves: "someone paid for this exact request body at this time."

But it doesn't prove which endpoint the payment was for. A receipt from GET /search?q=cats is technically valid for GET /search?q=dogs if both have empty bodies — the body hash matches.

The Fix: Request Context Binding

Add a request_context extension to the receipt:

{
  "request_body_hash": "sha256(...)",
  "timestamp": "2026-08-07T17:00:00Z",
  "signature": "0x...",
  "extensions": {
    "request_context": {
      "method": "GET",
      "path": "/search",
      "query_params_hash": "sha256(q=cats)",
      "amount_cents": 10
    }
  }
}

Now the receipt is bound to a specific operation, not just a body. The seller can reject a receipt that doesn't match the current request's method, path, and amount.

Pattern 2: Receipt Replay Protection — The Idempotency Cache

Once a receipt is valid, an attacker (or a buggy agent) could replay it. Without protection, the seller executes the same paid operation twice, and the buyer is charged once but gets two results.

The Fix: Receipt Nonce Cache

The seller stores sha256(receipt_signature) with a TTL matching the receipt's expiry window:

func (rc *ReceiptCache) CheckOrStore(sigHash string, ttl time.Duration) (*CachedResult, error) {
    rc.mu.Lock()
    defer rc.mu.Unlock()

    if cached, exists := rc.store[sigHash]; exists {
        if time.Now().Before(cached.ExpiresAt) {
            return &cached, ErrDuplicateReceipt // 409 Conflict
        }
        delete(rc.store, sigHash) // expired
    }
    return nil, nil // new receipt, proceed
}

The key insight: a replayed receipt returns 409 Conflict with the original response. The buyer gets the result they paid for; the seller doesn't re-execute. No double charge, no double execution.

Pattern 3: Timeout After Payment — The Hardest Case

The sequence:

  1. Buyer pays on-chain ✅
  2. Buyer sends request with receipt → seller starts processing
  3. Network timeout — seller's response never arrives
  4. Buyer doesn't know: did it work or not?

The Fix: Idempotent Retry with Receipt as Key

The buyer retries with the same receipt. The seller, having cached the result, returns it from the cache. If the seller never completed processing (crashed mid-execution), the receipt signature isn't in the cache yet, so the seller re-executes.

For side-effectful operations, use an explicit idempotency_key in the request body.

Pattern 4: The Failure Table

ScenarioBuyer ActionSeller BehaviorBuyer Gets
Payment confirmed, response receivedDoneNormal execution200 + result
Payment confirmed, response timeoutRetry with same receiptReplay from cache (409) or re-execute409 + cached result
Payment confirmed, seller crashed before cachingRetry with same receiptRe-execute200 + result
Receipt replayed by bugNormal requestCache hit → 409409 + original result
Receipt expired (TTL passed)New payment requiredCache miss → 402402 Payment Required
Payment sent to wrong addressNever gets to sellerN/ABuyer wallet shows sent; seller never sees it

What This Means for Agent Autonomy

An agent can now handle payments reliably:

1. POST /classify → 402 Payment Required
2. Pay $0.10 USDC on Base → tx confirmed
3. POST /classify + x402-receipt → 200 OK { "label": "spam" }
4. [Same POST + same receipt] → 409 Conflict { "label": "spam" }  ← no double charge
5. [POST + receipt, network timeout] → retry with same receipt → 409 + cached result

No human watches this loop. No double charges. No lost payments. The agent pays for exactly what it uses.


The patterns here are what we run in production at minia2a. The receipt nonce cache handles ~387K requests without a double-charge incident. The request_context extension is on our spec wishlist — it's the next logical step for receipt binding. If you're building on x402 and hitting these same problems, the idempotency cache is the highest-leverage change you can make.

Thanks to Swapnoneel Saha for the questions that prompted this writeup. Good engineering questions make better documentation.