USDG payments
The exact wire format for the x402 dance against Cloven. Use this if you're rolling your own HTTP client; the SDK in wallet mode handles all of this transparently.
Settlement is USDG on Robinhood Chain (chain id 4663). Cloven self-verifies the on-chain receipt — there is no facilitator in the loop, and no third-party x402 client library speaks this chain. See how it works for the honest scope.
The 402 response
When a request arrives without Authorization: Bearer … and without a valid X-402-Payment header, Cloven returns:
HTTP/1.1 402 Payment Required
Content-Type: application/json
{
"payment": {
"scheme": "x402",
"network": "robinhood",
"asset": "USDG",
"amount": "0.001000",
"recipient": "0xClovenTreasury...",
"validUntil": 1785240300,
"nonce": "3f1c5e0a-9d2b-4a17-8c31-6b0e2d4a7f55"
}
}Fields:
scheme— always"x402"for v1. Echo it back verbatim in the payment header.network— always"robinhood"(Robinhood Chain, id4663). Echo it back verbatim.asset— always"USDG". The contract is0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168, 6 decimals.amount— a decimal string in USDG units, e.g."0.001000"= 0.001 USDG. Not raw atomic units — convert to 6-decimal atomics yourself before callingtransfer.recipient— Cloven treasury wallet. Hard-pinned viaX402_RECIPIENT_ADDRESSenv var server-side; the client should trust the response and pay only this address.validUntil— Unix seconds. The transfer must confirm and be presented before this. Quote window is 5 minutes.nonce— server-generated UUID, one per quote.
Sign and broadcast
Sign a USDG transfer on Robinhood Chain for the quoted amount to the quoted recipient. Robinhood Chain is not shipped in viem/chains, so define it once:
import {
createPublicClient,
createWalletClient,
defineChain,
http,
parseUnits,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
export const robinhoodChain = defineChain({
id: 4663,
name: "Robinhood Chain",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrls: {
default: { http: ["https://rpc.mainnet.chain.robinhood.com"] },
},
blockExplorers: {
default: {
name: "Blockscout",
url: "https://robinhoodchain.blockscout.com",
apiUrl: "https://robinhoodchain.blockscout.com/api",
},
},
});
/** USDG (Paxos Global Dollar) on Robinhood Chain mainnet. 6 decimals. */
export const USDG = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168" as const;
const USDG_ABI = [
{
name: "transfer",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
] as const;Then broadcast the transfer:
const account = privateKeyToAccount(process.env.AGENT_PK as `0x${string}`);
const rpc = process.env.ROBINHOOD_RPC_URL ?? "https://rpc.mainnet.chain.robinhood.com";
const wallet = createWalletClient({ account, chain: robinhoodChain, transport: http(rpc) });
const publicClient = createPublicClient({ chain: robinhoodChain, transport: http(rpc) });
// `amount` is the decimal string from the quote, e.g. "0.001000"
const txHash = await wallet.writeContract({
address: USDG,
abi: USDG_ABI,
functionName: "transfer",
args: [recipient, parseUnits(amount, 6)],
});
await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1 });The public RPC above is a keyless read fallback only — Robinhood's terms bar it from production or high-throughput use. Point ROBINHOOD_RPC_URL at a provisioned endpoint in production.
The wallet needs ETH for gas (Robinhood Chain's native token) as well as USDG for the payment. 1 confirmation is enough: blocks land in ~50–100ms, so waiting is nearly free.
You can eyeball any tx at https://robinhoodchain.blockscout.com/tx/<txHash>.
The X-402-Payment header
After the tx confirms, retry the original request with X-402-Payment set to a base64-encoded JSON payload:
GET /v1/fresh?pack=crypto HTTP/1.1
Host: api.cloven.cloud
X-402-Payment: eyJzY2hlbWUiOiJ4NDAyIiwibmV0d29yayI6InJvYmluaG9vZCIsInR4SGFzaCI6IjB4YWJjLi4uIiwicGF5ZXIiOiIweFdhbGxldCJ9The decoded payload:
{
"scheme": "x402",
"network": "robinhood",
"txHash": "0xabc...",
"payer": "0xWallet..."
}Fields:
scheme— must be"x402". Anything else →400 wrong_scheme.network— must be"robinhood". Anything else, including a stale"base", →400 wrong_network.txHash— the confirmed USDG transfer tx. 0x-prefixed, 32 bytes (66 chars).payer— the wallet that signed the tx. 0x-prefixed, 20 bytes (42 chars). Used for the on-chainTransfer.frommatch, for stake-tier discount lookup (Phase 3), and for trace attribution.
The header must be base64 of the JSON. Naked JSON in the header is rejected. X-Payment is accepted as a legacy alias for the same header; prefer X-402-Payment.
Server verifies
Cloven verification order (any failure → a typed error code):
- Header parse. Base64 decode → JSON parse → required fields present. Failure:
400 invalid_payment_header. - Envelope checks.
network === "robinhood",scheme === "x402", tx hash and payer well-formed. Failure:400 wrong_network/400 wrong_scheme/400 invalid_tx_hash/400 invalid_payer. - Idempotency check.
redis.get("x402:tx:robinhood:" + txHash). If set, accept the cached settlement (idempotent replay). If fresh — proceed. - On-chain lookup.
eth_getTransactionReceipt(txHash)against Robinhood Chain. Failure (RPC down, hash unknown):400 tx_not_found. - Status check. Receipt status is
success. Otherwise:400 tx_reverted. - Age check. The tx's
block.timestampis within the last 10 minutes. Otherwise:400 tx_too_old. - Event check. Receipt logs include at least one
Transfer(from = payer, to = recipient, value)event emitted by the USDG contract, and the summedvalue ≥ quoted amount. Otherwise:400 no_stable_transfer_in_txor400 insufficient_payment. - Cache idempotency. Set
x402:tx:robinhood:<txHash>in Redis with 24h TTL. - Serve. 200 OK with the MindResponse.
The idempotency key is scoped by settlement network, so a tx hash settled on the retired Base rail can never be replayed against Robinhood Chain.
Successful response
HTTP/1.1 200 OK
Content-Type: application/json
{ "state": {...}, "brief": "...", "citations": [...], "freshness": {...} }Idempotency rules
- The 24-hour cache lets you retry the API call freely with the same
X-402-Paymentheader — useful if the response was lost in transit. - Re-using a tx hash from a different call (same hash, different endpoint / pack / args) is fine. The payment is "I paid for one API call within 24h."
- After 24h, the cache purges. The tx becomes "too old" on the next attempt — pay fresh.
Underpayment
If the summed on-chain Transfer.value to the treasury is less than the quoted amount, the tx is not partially credited. Cloven returns:
{
"error": "insufficient_payment: paid 0.000500 USDG, need 0.001000 USDG"
}The underpayment USDG stays in the Cloven treasury — there is no refund mechanism. Don't underpay.
Wrong recipient / wrong network
If the tx was signed to a different recipient (typo, frontrun, replay attack), Cloven cannot credit it — no matching Transfer log is found and the request fails with no_stable_transfer_in_tx. A payment header declaring a network other than "robinhood" is rejected outright with wrong_network before any RPC call is made.
A transfer broadcast on the wrong chain is simply invisible to Cloven: the tx hash will not resolve on Robinhood Chain and you'll get tx_not_found. The funds are gone to wherever you sent them. The client wrote the tx — it's the client's responsibility to be on chain 4663 and to pay the address from the live quote.
Rate limit
Even paid requests pass through the Redis sliding-window rate limit (anti-DDoS). Default 100 req/min per wallet. Diamond stake tier raises this to 1000 req/min (Phase 3).