Define idempotent retry semantics for `/verify` and `/settle`
Summary
-
/verifyand/settleare POST endpoints but the spec never states how facilitators must deal with duplicate requests, so recoverable network errors can produce inconsistent retries. - Each
PaymentPayloadalready carries a unique EIP-3009 nonce; using the canonical payload as the idempotency key would let facilitators return the original response instead of surfacing contract-level nonce errors. - Clarifying the requirement in specs/x402-specification.md and aligning the reference tooling gives integrators a predictable retry contract without inventing new protocol surface.
Background
-
specs/x402-specification.md
POST /verifyandPOST /settlebut omits expected behaviour for repeated submissions of the same payload. - Once settlement succeeds, the authorization nonce is consumed on-chain. Retrying the same payload after a transient failure eventually triggers an
AuthorizationUsedrevert in the ERC-3009 contract, which current facilitators surface assuccess: false. - Reference implementations (e.g. typescript/packages/x402/src/schemes/exact/evm/facilitator.ts) do not persist prior results, so applications cannot distinguish “already settled” from genuine failures.
Proposal
- Spec change
- Update
POST /verifyandPOST /settleto state that facilitators MUST treat the canonicalpaymentPayloadas the idempotency key. The first submission performs validation/settlement; subsequent identical submissions return the originalisValid/successpayload (includingtransactionwhen available). - Add a normative error code (e.g.
already_settled) that facilitators return when they detect the payload has already succeeded, so clients can stop retrying without triggering compensation flows.
- Update
Notes
- Treating the signed payload as the idempotency key keeps the protocol self-contained across HTTP, MCP, and any other transport that forwards the same payload.
+1
+1
Thank you all for your input. My two cents: Idempotency for duplicate payments tied to the same request should be handled at the application level, which is responsible for bypassing /verify and /settle altogether.
Only the application has enough context and business logic to determine what constitutes a new payable request versus one that should reuse a previously executed payment.
The aim is for facilitators to remain stateless.
Let me know if you disagree
@fabrice-cheng thank you for your comment. I understand the intent to keep facilitators stateless. it makes sense to keep the spec thin, and I agree that enforcing idempotency (e.g., via AuthorizationUsed) at the spec level might be too rigid.
That said, most major payment platforms like Stripe or PayPal do support idempotency on the platform side (Idempotency-Key, PayPal-Request-Id, etc.). In practice, I imagine many facilitators will end up needing a similar mechanism to handle retries safely.
Thanks. I see your point. This might be a worthy topic to bring to the V2 for the facilitator spec. May I suggest you post this exact comment in the V2 spec PR #446 ?
I'll also bring it internally with the team (tradeoff stateless)
@fabrice-cheng Sounds great, thanks for considering it! I’ll post a summary comment in the V2 spec PR https://github.com/coinbase/x402/pull/446 as you suggested. Appreciate you also bringing it up internally.