Verification flow

How the Cloven server verifies an x402 payment. Useful if you're auditing the protocol, building a competing service, or debugging why a payment was rejected.

Fail-closed semantics

x402 verification is strictly fail-closed: any condition that cannot be positively confirmed → 402. There is no "we'll let it through this time." This matters because the failure modes are not symmetric — accepting an invalid payment costs Cloven real revenue and corrupts the trace ledger; rejecting a valid payment is recoverable (the client retries).

The eight verification gates, in order:

1.  Header present                      missing → 402 payment_required
2.  Header parseable                    fail    → 400 invalid_payment
3.  Required fields                     fail    → 400 invalid_payment
4.  Idempotency cache hit               hit     → accept (Step 8 only)
5.  On-chain receipt                    miss    → 402 tx_not_found
6.  Status 0x1                          fail    → 402 tx_reverted
7.  USDC.Transfer event matches         fail    → 402 insufficient_payment | wrong_recipient
8.  Age ≤ 10 min                        fail    → 402 tx_too_old
9.  Cache + serve                       —       → 200 OK

Block age cap (10 minutes)

The on-chain block.timestamp of the tx must be within 10 minutes of the request's arrival. Older transactions are rejected with 402 tx_too_old.

Why 10 minutes:

  • Anti-replay. A leaked X-Payment header is worthless to an attacker after 10 minutes. The valid window is short enough to neutralise log-scrape attacks.
  • Generous enough for slow RPCs. Base block time is ~2s; even a sluggish RPC + a slow client can comfortably complete sign+broadcast+confirm+retry within 10 minutes.
  • Aligns with the 5-minute pack pulse cadence. The data freshness contract is 5 minutes; the payment freshness contract is 2× that — both within the same operational window.

If a client paid > 10 minutes ago, it should treat the payment as lost (or, more accurately, donated to the Cloven treasury — there is no refund). The fix is "re-quote, pay fresh, retry."

Idempotency cache (24h TTL)

A successful verification writes x402:tx:<txHash> to Redis with a 24-hour TTL. Subsequent requests bearing the same X-Payment header hit the cache and skip Steps 5-8 entirely — the response is served as if the tx had just verified.

This means a client can:

  • Retry the API call freely for 24 hours with the same payment.
  • Recover from network failures between Step 4 and Step 5 by replaying.
  • Get "free" reads against the same endpoint within the 24-hour window — yes, this is intentional. A single payment buys one delivery of the response; the client may need to re-fetch if their process crashes.

It also means an attacker who steals an X-Payment header from server logs gets at most 24 hours of free reads against that endpoint (mitigated by the 10-minute on-chain age cap — after 10 minutes the cache is the only path, and after 24 hours nothing works). The threat model is: keep your logs clean.

Event matching

Step 7 requires a USDC.Transfer(from, to, value) event log in the tx receipt where:

  • from == payer (the payer field in the X-Payment header).
  • to == X402_RECIPIENT_ADDRESS (Cloven treasury — pinned server-side).
  • value >= quoted amount.
  • The emitting contract is the canonical Base USDC contract (0x833589fCD…).

We match against the event log, not the tx value field, because USDC transfers use ERC-20 transfer() — the ETH value of the transaction is always 0. The Transfer event is the real signal.

We tolerate value greater than the quote (overpayment) silently — no refund, but the request goes through. Don't overpay.

RPC redundancy

X402_RPC_URL points at the primary Base RPC provider (Alchemy, Infura, or QuickNode in our setup). If the primary fails Step 5, we fall back to a secondary provider. A request that errors past both providers returns 503 rpc_unavailable rather than 402 — the client should retry the call (not the payment) on backoff.

We do NOT retry verification across providers if any one returns "tx not found" with a high-confidence response — that conclusively means the tx doesn't exist or hasn't propagated yet. Re-quote and re-pay.

Confirmations

Step 5 expects at least 1 confirmation. Base finality is fast enough that 1-conf is the recommended setting. The X402_MIN_CONFIRMATIONS env var allows operators to raise this for higher-value flows, but the default is 1.

Reorgs on Base are extremely rare and the value at stake per call ($0.001–$0.01) does not justify additional confirmation overhead.

What gets logged

Every verification attempt — success or failure — writes a row to the traces table tagged surface: "rest"|"mcp"|"sdk", paid_via: "x402", with the events array recording each gate's outcome:

{
  type: "x402_verify",
  txHash: "0xabc...",
  result: "ok" | "tx_too_old" | "insufficient_payment" | "wrong_recipient" | ...,
  payer: "0xWallet...",
  amount_paid_usd: 0.001,
  ts: "2026-05-24T12:00:00Z"
}

Useful for billing reconciliation (run a SUM on amount_paid_usd over your wallet), abuse forensics (multiple tx_too_old from one payer → bot misbehavior), and the Phase 3 Commons revshare ledger.

Discount verification (Phase 3)

When the $CLOVEN token is live, the quote in Step 2 may include a discounted amount. The server records the discount tier in the trace event so the revenue split routes correctly: (quoted_amount × 50%) to buyback-and-burn, etc. The discount itself comes off the infra + team share, not the buyback share — the burn budget is calibrated against the discounted post-tax revenue.

Phase 1 discount lookup returns 0% for every wallet — the code is wired but dormant.