An agent calls a paid API. The payment settles. The response never arrives. The agent retries. Now you have two charges for one result.
This is not a hypothetical edge case. When agents orchestrate paid API calls (Stripe, payment gateways, credit-based services, on-chain transfers), network failures between payment confirmation and response receipt become a financial liability. The agent sees a timeout, assumes failure, and does what it was trained to do: retry.
A team running an MCP gateway where agents pay per call in USDC (over x402 on Base and MPP on Tempo) published their four-layer approach to preventing double charges. This is not about authorization limits or spending caps. It is about transactional safety when the agent itself is the payer and the orchestrator.
The Problem Space
The failure mode is specific:
- Agent submits payment authorization
- Payment provider processes and settles
- Network drops before response reaches agent
- Agent sees timeout, retries with new payment
- Same work, two charges
Traditional web applications solve this with idempotency keys. The client generates a unique identifier, attaches it to the request, and the server deduplicates. But agents introduce new constraints:
- The agent may not persist state between retries (ephemeral runtime, crash recovery)
- The payment authorization itself may be cryptographically signed (EIP-3009, payment credentials)
- The orchestrator may not control the payment provider’s deduplication logic
- Webhook delivery is unreliable, so you cannot depend on async confirmation
Layer 1: Single-Use Payment Authorizations
The first line of defense is cryptographic. With x402 on Base, the agent does not send money directly. It signs an authorization for a USDC transfer using EIP-3009. Each authorization carries a nonce, and the token contract accepts a given nonce from a given payer only once.
If the agent resends the exact same signed payment, the second transfer fails on-chain. No server-side bookkeeping is needed for that guarantee. It holds even if the gateway’s own records are lost.
MPP has the same property. A credential that was already used returns 403 credential_replayed, and the call is not charged.
What this solves: Double charging when the agent retries with the identical payment object.
What it does not solve: The agent still has no result. It paid, the payment was accepted once, and now resending fails. The agent is stuck.
Layer 2: Payment Identifiers for Result Caching
To fix the stuck agent, the gateway accepts a payment_id (16 to 128 characters) attached to the payment authorization. This is the x402 payment-identifier extension.
The rules:
- Same
payment_id, same payment: return the stored answer from the first request. No second verify, no second settle. - Same
payment_id, different payment: reject with409 Conflict. The agent is trying to reuse an identifier for a different transaction. - New
payment_id: process normally.
The agent generates the payment_id before the first attempt. If the response is lost, it retries with the same payment_id and a new payment authorization (new nonce). The gateway recognizes the identifier, skips the API call, and returns the cached result.
What this solves: Retry without double charging, as long as the agent remembers the payment_id.
What it does not solve: If the agent crashes or loses state between attempts, it generates a new payment_id and pays again.
Layer 3: Deterministic Identifiers from Request Content
To handle state loss, the gateway supports deterministic payment_id generation. The agent hashes the request parameters (endpoint, method, body) and uses that hash as the payment_id.
import hashlib
import json
def generate_payment_id(endpoint: str, method: str, body: dict) -> str:
payload = json.dumps({
"endpoint": endpoint,
"method": method,
"body": body
}, sort_keys=True)
return hashlib.sha256(payload.encode()).hexdigest()[:32]
Now the agent does not need to persist anything. If it retries the same request after a crash, it generates the same payment_id, and the gateway returns the cached result.
What this solves: Idempotency across agent restarts and state loss.
What it does not solve: If the agent intentionally wants to call the same endpoint with the same parameters twice (legitimate duplicate), the deterministic identifier prevents it. The agent must add a client-side nonce or timestamp to the request body to force a new payment_id.
Layer 4: Server-Side Deduplication Windows
The gateway also tracks payment authorizations by their cryptographic signature for a rolling 24-hour window. If the same signed authorization appears twice (even with different payment_id values), the second is rejected.
This catches agent bugs where the payment_id generation logic is broken but the payment authorization is reused. It is a backstop, not a primary mechanism.
What this solves: Defense against agent implementation errors.
What it does not solve: Legitimate retries after 24 hours with the same payment object will fail. The agent must generate a new authorization.
Architecture: Where Each Layer Lives
| Layer | Location | Failure Mode |
|---|---|---|
| Single-use nonces | On-chain token contract | Agent stuck with no result after first success |
| Payment identifiers | Gateway application state | Agent loses state, generates new ID, pays twice |
| Deterministic IDs | Agent-side generation logic | Prevents legitimate duplicate calls unless body varies |
| Signature tracking | Gateway deduplication cache | 24-hour window means old authorizations eventually expire |
The payment identifier is the primary mechanism. The other three layers are either foundational (nonces) or defensive (deterministic generation, signature tracking).
Implementation Notes
Where to generate the payment ID: In the agent’s tool wrapper, not the orchestrator. The tool wrapper knows the API call parameters before invoking the payment flow. The orchestrator only sees high-level intent.
Where to store the result cache: In the gateway’s application database, keyed by payment_id. TTL should match your longest expected retry window (typically 1 hour to 24 hours). Use Redis or a similar key-value store for fast lookups.
What to cache: The full HTTP response (status, headers, body). Do not cache only the success case. If the API returned a 500, the agent should see that 500 on retry, not pay again for a second 500.
Webhook reconciliation: If the payment provider sends async confirmation webhooks, log them but do not depend on them for idempotency. Webhooks can arrive out of order, late, or not at all. The synchronous response path is the source of truth.
Observability: Emit metrics for:
payment_idcollisions (same ID, different payment)- Cached result hits (retry avoided)
- Signature reuse attempts (agent bug or attack)
When This Pattern Breaks
Non-idempotent APIs: If the downstream API itself is not idempotent (e.g., “send an email”), caching the result does not prevent duplicate side effects. You need request deduplication at the API provider level, or you need to wrap the API in your own idempotent layer.
Long-running operations: If the API call takes 10 minutes and the agent times out after 30 seconds, the cached result will not be ready when the agent retries. You need async job tracking with a separate result-polling endpoint.
Multi-step transactions: If the agent orchestrates a sequence of paid calls (pay for A, then pay for B), a failure in the middle leaves partial state. Idempotency per call does not give you atomicity across the sequence. You need saga patterns or compensating transactions.
Technical Verdict
Use payment identifiers when your agents call paid APIs that you control or wrap. The pattern is simple, requires minimal state, and works across agent restarts.
Do not use this pattern if:
- The downstream API is not idempotent and you cannot wrap it
- The API call duration exceeds your agent’s retry timeout (use async jobs instead)
- You need atomicity across multiple paid calls (use saga orchestration)
The deterministic identifier approach (Layer 3) is optional. It adds complexity and prevents legitimate duplicate calls. Use it only if your agents frequently lose state between retries and you cannot fix the state persistence problem.
The signature tracking (Layer 4) is cheap insurance. Add it if you are handling real money and want defense against agent bugs.