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.

Cloven verifies the receipt itself, on-chain, with viem against Robinhood Chain (id 4663). No facilitator, no third-party attestation service — the chain is the only source of truth in this path.

Fail-closed semantics

x402 verification is strictly fail-closed: any condition that cannot be positively confirmed is rejected. 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 verification gates, in order:

1.  Header present                      missing → 402 + PaymentRequirements
2.  Header parseable (base64 JSON)      fail    → 400 invalid_payment_header
3.  network === "robinhood"             fail    → 400 wrong_network
4.  scheme === "x402"                   fail    → 400 wrong_scheme
5.  txHash / payer well-formed          fail    → 400 invalid_tx_hash | invalid_payer
6.  Idempotency cache hit               hit     → accept, skip to serve
7.  On-chain receipt                    miss    → 400 tx_not_found
8.  Receipt status "success"            fail    → 400 tx_reverted
9.  Block age ≤ 10 min                  fail    → 400 tx_too_old
10. USDG Transfer event matches         fail    → 400 no_stable_transfer_in_tx
                                                 | insufficient_payment
11. Cache + serve                       —       → 200 OK

Note the network and scheme checks run before any RPC call — a header from the retired Base rail is rejected without touching the chain.

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 tx_too_old.

Why 10 minutes:

  • Anti-replay. A leaked X-402-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. Robinhood Chain blocks land in ~50–100ms; even a sluggish RPC + a slow client can comfortably complete sign+broadcast+confirm+retry within 10 minutes. On this chain the window is enormously generous.
  • 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:robinhood:<txHash> to Redis with a 24-hour TTL. Subsequent requests bearing the same X-402-Payment header hit the cache and skip the on-chain gates entirely — the response is served as if the tx had just verified.

The key is scoped by settlement network. A tx hash cached under the retired Base rail cannot be replayed against Robinhood Chain, and vice versa.

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-402-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

The event gate requires a Transfer(from, to, value) event log in the tx receipt where:

  • The emitting contract is the USDG contract on Robinhood Chain (0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168, overridable server-side via STABLE_CONTRACT for testnet / mock deployments).
  • topics[0] is the canonical ERC-20 Transfer(address,address,uint256) topic.
  • from == payer (the payer field in the X-402-Payment header).
  • to == X402_RECIPIENT_ADDRESS (Cloven treasury — pinned server-side).
  • The sum of matching values across all such logs in the receipt is >= quoted amount.

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

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

USDG has 6 decimals, identical to USDC, so amount arithmetic is unchanged from the previous rail.

RPC configuration

ROBINHOOD_RPC_URL points at the settlement RPC. In production this should be a provisioned Robinhood Chain endpoint; the public https://rpc.mainnet.chain.robinhood.com is a keyless read fallback only and Robinhood's terms bar it from production or high-throughput use.

The viem client is pinned to chain 4663. Pointing its transport at any other chain's RPC would make every read hit the wrong chain and silently fail verification — which is why there is no fallback to a legacy BASE_RPC_URL. A half-migrated environment must set ROBINHOOD_RPC_URL explicitly.

If the RPC cannot return a receipt, verification fails closed with tx_not_found. That is deliberate: we do not guess. The client should retry on backoff, or re-quote and re-pay if the quote has expired.

Confirmations

Verification reads the receipt, which implies at least 1 confirmation. Robinhood Chain blocks land in ~50–100ms, so a 1-conf wait costs the client essentially nothing and is the recommended setting.

Robinhood Chain is an Arbitrum Orbit L2; reorgs at this depth are not a practical concern, 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" | "no_stable_transfer_in_tx" | ...,
  payer: "0xWallet...",
  amount_paid_usd: 0.001,
  ts: "2026-07-28T12:00:00Z"
}

amount_paid_usd is the USDG amount — 1:1 USD, so the figure is a dollar amount directly.

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 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. The token's launch chain, staking contract and buyback routing are TBD and to be re-spec'd for Robinhood Chain; the earlier Base-native design does not carry over.