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 OKNote 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-Paymentheader 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 viaSTABLE_CONTRACTfor testnet / mock deployments). topics[0]is the canonical ERC-20Transfer(address,address,uint256)topic.from == payer(thepayerfield in theX-402-Paymentheader).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.