August 7, 2026
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.
An x402 payment has three phases:
402 Payment Required with payment detailsx402-receipt header, seller verifies on-chain confirmation, returns resultEach 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.
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.
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.
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 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.
The sequence:
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.
| Scenario | Buyer Action | Seller Behavior | Buyer Gets |
|---|---|---|---|
| Payment confirmed, response received | Done | Normal execution | 200 + result |
| Payment confirmed, response timeout | Retry with same receipt | Replay from cache (409) or re-execute | 409 + cached result |
| Payment confirmed, seller crashed before caching | Retry with same receipt | Re-execute | 200 + result |
| Receipt replayed by bug | Normal request | Cache hit → 409 | 409 + original result |
| Receipt expired (TTL passed) | New payment required | Cache miss → 402 | 402 Payment Required |
| Payment sent to wrong address | Never gets to seller | N/A | Buyer wallet shows sent; seller never sees it |
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.