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