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, id 4663). Echo it back verbatim.
  • asset — always "USDG". The contract is 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168, 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 calling transfer.
  • recipient — Cloven treasury wallet. Hard-pinned via X402_RECIPIENT_ADDRESS env var server-side; the client should trust the response and pay only this address.
  • validUntilUnix 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: eyJzY2hlbWUiOiJ4NDAyIiwibmV0d29yayI6InJvYmluaG9vZCIsInR4SGFzaCI6IjB4YWJjLi4uIiwicGF5ZXIiOiIweFdhbGxldCJ9

The 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-chain Transfer.from match, 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):

  1. Header parse. Base64 decode → JSON parse → required fields present. Failure: 400 invalid_payment_header.
  2. 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.
  3. Idempotency check. redis.get("x402:tx:robinhood:" + txHash). If set, accept the cached settlement (idempotent replay). If fresh — proceed.
  4. On-chain lookup. eth_getTransactionReceipt(txHash) against Robinhood Chain. Failure (RPC down, hash unknown): 400 tx_not_found.
  5. Status check. Receipt status is success. Otherwise: 400 tx_reverted.
  6. Age check. The tx's block.timestamp is within the last 10 minutes. Otherwise: 400 tx_too_old.
  7. Event check. Receipt logs include at least one Transfer(from = payer, to = recipient, value) event emitted by the USDG contract, and the summed value ≥ quoted amount. Otherwise: 400 no_stable_transfer_in_tx or 400 insufficient_payment.
  8. Cache idempotency. Set x402:tx:robinhood:<txHash> in Redis with 24h TTL.
  9. 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-Payment header — 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).