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.