Skip to content

Security considerations

x402 moves money on every request. Security mistakes here cost real funds. This page outlines the threat model and practical mitigations.

Client (untrusted) ──► Your Server (trusted) ──► Facilitator (trusted for settlement)
──► Chain (trustless / consensus)
  • Never trust the client to report payment success. The server must verify via the facilitator or on-chain before releasing resources.
  • The facilitator is a trusted third party for verification and settlement. Choose one with a security track record and audit history.
  • Your server is the policy layer: it decides what requires payment, how much, and whether verification is sufficient to release data.

EIP-3009 transferWithAuthorization uses a random nonce (32 bytes). Once the facilitator submits the transfer on-chain, that nonce is marked as used in the token contract — replaying the same signature reverts.

Your responsibilities:

  • Generate nonces with crypto.getRandomValues, never incrementally.
  • Don’t cache or log full payment signatures in a way that could be exfiltrated and replayed before settlement.
  • If your server receives the same payment-signature twice (e.g. client retry), the facilitator’s settle call should be idempotent or return an error. Handle both cases.

Set validBefore as tight as practical. For a single API request, 5–15 minutes is reasonable. One hour is the upper bound for interactive flows. Long-lived authorizations increase the window for front-running.

validAfter can be 0 (immediately valid) for typical request-response flows.

If the payment-signature header is intercepted in transit (e.g. no TLS), an attacker could submit the authorization before your server does. Mitigations:

  • Always use HTTPS in production.
  • The to field in the authorization is fixed to the merchant address — an attacker can’t redirect funds, but they could cause settlement to happen before your server calls the facilitator (wasting your user’s authorization).
  • Some facilitators support receiveWithAuthorization which restricts the caller to the to address, preventing third-party submission.

The client constructs the payment based on the 402 response body. A malicious client could sign a lower amount. Your server must not trust the amount in the payment-signature — the facilitator’s verify step should reject mismatches. Confirm your facilitator validates amount ≥ maxAmountRequired.

You’re trusting the facilitator to:

  1. Honestly verify that the signature is valid and the amount is correct.
  2. Actually settle the transfer on-chain to your payTo address.
  3. Not front-run or redirect funds.

Mitigations:

  • Use established facilitators (Coinbase CDP, thirdweb) with public APIs and audit trails.
  • Verify settlement independently: check your payTo balance or query the chain for the txHash the facilitator returns.
  • Log the txHash and periodically reconcile against on-chain records.
SecretWhere it livesExposure risk
facilitatorUrlServer envIf leaked, attacker could call verify/settle directly (low risk — they’d need valid signatures)
payTo addressServer config + 402 bodyPublic by design (clients need it to sign)
TallyPay apiKey (tp_live_…)Server + client envIf leaked, attacker can send fake trace events. Not a payment risk, but pollutes your dashboard data
Wallet private keyNever on your serverIf you’re self-hosting a facilitator, guard this with HSM / KMS
  • Never log raw payment-signature values. They contain signed authorizations that may not yet be settled.
  • Do log traceId, txHash, facilitator response codes, and your own request IDs.
  • Treat facilitator responses as untrusted input when constructing error messages for clients.
  • HTTPS everywhere (mixed content breaks wallet libraries)
  • Separate testnet and mainnet facilitator URLs, payTo addresses, and TallyPay API keys
  • validBefore set to < 15 minutes for standard flows
  • Reconciliation: periodically verify txHash values against on-chain state
  • Rate limiting on your 402 endpoints to prevent abuse (repeated 402s are cheap for an attacker)
  • CORS configured to your own origins only (don’t wildcard in production)
  • Error responses never leak facilitator internals or raw signatures