The x402 payment rail for USDG on Robinhood Chain (chain ID 4663).
Spec-conformant exact/EIP-3009 server middleware, a paying client, and a self-hostable
facilitator. Gasless USDG micropayments over plain HTTP 402 — no accounts, no API keys, no
subscriptions. hood402 follows the standard x402 protocol
wire format exactly, so it interoperates with the wider x402 client ecosystem, not just its
own client.
Docs: https://github.com/nirholas/robinhood-chain-x402 (the reference pages under
docs/ render from the repo; the hosted docs site is not published
right now, so the repo is the canonical source).
USDG (Paxos Global Dollar) is a facet/diamond-router stablecoin. Its base implementation
doesn't expose EIP-3009 directly, but getFacet(bytes4) proves the facet is registered —
verified live against both networks:
npm run verify:usdgrobinhood (chain 4663) — USDG 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
getFacet(transferWithAuthorization) -> 0x780d30b6a89BC9Eef953a543aA288c3B05b01309 [OK]
getFacet(receiveWithAuthorization) -> 0x780d30b6a89BC9Eef953a543aA288c3B05b01309 [OK]
getFacet(authorizationState) -> 0x780d30b6a89BC9Eef953a543aA288c3B05b01309 [OK]
DOMAIN_SEPARATOR() -> 0x7a3d7400b27830f4f91c2c16a082486d67c1befecaec2f53b33f1f35d5b62036
Expected domain: name="Global Dollar", version="1"
robinhood-testnet (chain 46630) — USDG 0x7E955252E15c84f5768B83c41a71F9eba181802F
getFacet(transferWithAuthorization) -> 0x08f560a85db40a7d4ac49b4F44f1D38e5B8aB811 [OK]
...
PASS: USDG EIP-3009 facet registration confirmed on both networks.
The EIP-712 domain separator was reconstructed offline (name="Global Dollar",
version="1") and matches the live DOMAIN_SEPARATOR() on both chains exactly. This is
the load-bearing decision behind hood402: settlement is the standard, gasless
transferWithAuthorization path — the same mechanism USDC uses, and the one x402's exact
scheme is designed around. No custom scheme, no proxy contract, no Permit2 fallback needed.
See Blockscout — mainnet USDG
and docs/index.html for the full write-up.
npm install hood402 viemNode ≥ 20. To work against an unreleased change instead, install from a checkout:
npm i ../robinhood-chain-x402.
| Path | What it is |
|---|---|
hood402 (this package) |
Protocol types, the exact/EIP-3009 scheme, verifyPayment/settlePayment, and the hood402/server + hood402/client subpath exports |
facilitator/ |
A standalone, self-hostable facilitator service — /verify, /settle, /supported, /metrics, an idempotent SQLite ledger, and a Dockerfile |
import express from 'express'
import { paywall } from 'hood402/server'
const app = express()
app.get('/premium', paywall({
price: '0.01', // USDG
payTo: '0xYourReceivingAddress',
network: 'robinhood', // or 'robinhood-testnet'
facilitator: 'https://your-facilitator.example.com',
}), (req, res) => {
res.json({ data: 'unlocked after a settled USDG payment' })
}) No facilitator? Pass wallet (a viem WalletClient holding a gas key), account, and
reader (a viem PublicClient) instead — the server verifies and settles locally:
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { robinhood } from 'viem/chains'
import { paywall } from 'hood402/server'
const account = privateKeyToAccount(process.env.FACILITATOR_PRIVATE_KEY as `0x${string}`)
const transport = http('https://rpc.mainnet.chain.robinhood.com')
app.get('/premium', paywall({
price: '0.01',
payTo: '0xYourReceivingAddress',
network: 'robinhood',
wallet: createWalletClient({ account, chain: robinhood, transport }),
account: account.address,
reader: createPublicClient({ chain: robinhood, transport }),
}), (req, res) => res.json({ data: 'unlocked' }))Hono works too — use honoPaywall from hood402/server with the same options.
import { Hood402Client, fromAccount } from 'hood402/client'
import { privateKeyToAccount } from 'viem/accounts'
const account = privateKeyToAccount(process.env.ROBINHOOD_CHAIN_PRIVATE_KEY as `0x${string}`)
const client = new Hood402Client({
signer: fromAccount(account),
maxSpendPerOrigin: '1.00', // hard cap in USDG — refuses to sign above it
})
const res = await client.fetch('https://api.example.com/premium')
console.log(await res.json()) // the 402 was paid automaticallyclient.fetchWithReceipt(url) returns the response and the decoded
X-PAYMENT-RESPONSE settlement receipt (transaction hash, network, payer). In the
browser, build a Signer from an injected wallet with fromWalletClient instead of
fromAccount.
| Network id | Chain ID | USDG address | Explorer |
|---|---|---|---|
robinhood (mainnet) |
4663 | 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 |
Blockscout |
robinhood-testnet |
46630 | 0x7E955252E15c84f5768B83c41a71F9eba181802F |
Blockscout |
Both are 6-decimal USDG with EIP-712 domain name="Global Dollar", version="1". Aliases like
"robinhood-chain", "robinhood-mainnet", "robinhood-sepolia", eip155:4663/eip155:46630,
and the bare chain ids 4663/46630 all resolve to the same two networks via
resolveNetwork()/requireNetwork().
Protocol primitives shared by both the server and client. Re-exported by hood402/server and
hood402/client where relevant.
| Export | What it does |
|---|---|
X402_VERSION, SCHEME |
Wire-protocol constants (1, "exact"). |
PaymentRequirements, PaymentRequiredResponse, ExactEvmAuthorization, ExactEvmPayload, PaymentPayload, VerifyResult, SettleResult, SettlementResponseHeader, SupportedResponse |
Wire-format TypeScript types. |
InvalidReason, REASON_TEXT |
The canonical failure-reason union and its human-readable text (e.g. authorization_already_used → "The authorization nonce has already been settled (replay)."). |
Hood402ConfigError, SpendCapExceededError |
Errors thrown on misconfiguration / an over-cap client payment. |
ROBINHOOD_MAINNET, ROBINHOOD_TESTNET, NETWORKS, HoodNetwork |
The network registry — chain id, RPC URL, explorer, USDG address/decimals/domain. |
resolveNetwork(idOrChainId) |
Resolve a network by id/alias/chain id; returns undefined if unknown. |
requireNetwork(idOrChainId) |
Same, but throws a descriptive error on an unknown network. |
PAYMENT_HEADER, PAYMENT_RESPONSE_HEADER |
The literal header names "X-PAYMENT" / "X-PAYMENT-RESPONSE". |
Price |
string | { atomic: string } | { usdg: string } — how a resource's price can be expressed. |
toAtomic(price, decimals) |
Convert a Price to an atomic-unit decimal string. |
buildRequirements(input) |
Build a spec-compliant PaymentRequirements for a resource (price, payTo, network, description, …). |
buildPaymentRequired(accepts, error?) |
Wrap one or more PaymentRequirements into a 402 response body. |
TRANSFER_WITH_AUTHORIZATION_TYPES |
The EIP-712 TransferWithAuthorization type definition. |
usdgDomain(net) |
Build the EIP-712 domain for a network's USDG contract. |
randomNonce() |
Generate a fresh random 32-byte EIP-3009 nonce (browser + Node safe). |
authorizationMessage(auth) |
Convert an ExactEvmAuthorization (decimal-string fields) into its EIP-712 message form (bigint fields). |
verifyAuthorizationSignature(net, auth, signature) |
Verify an EIP-3009 signature recovers to auth.from (EOA + ERC-1271). |
isWellFormedPayload(p) |
Type-guard: does p have every required PaymentPayload field, correctly typed? |
validateStructural(payload, requirements, now?) |
The pure, RPC-free half of verification — scheme/network/asset/recipient/amount/expiry checks. |
verifyPayment(opts) |
Full verification state machine: structural → signature → (with a reader) replay → balance. Returns VerifyResult. |
settlePayment(opts) |
Re-verify, then broadcast transferWithAuthorization and await the receipt. Returns SettleResult. |
encodeBase64Json, decodeBase64Json, encodePaymentHeader, decodePaymentHeader, encodeSettlementHeader, decodeSettlementHeader |
Base64/JSON codecs for the X-PAYMENT and X-PAYMENT-RESPONSE header values. |
FacilitatorClient |
HTTP client for a remote facilitator — supported(), verify(payload, requirements), settle(payload, requirements). |
eip3009Abi, erc20Abi |
The minimal viem ABI fragments used for transferWithAuthorization, authorizationState, balanceOf, decimals. |
| Export | What it does |
|---|---|
paywall(opts: PaywallOptions) |
Express middleware. Verifies X-PAYMENT, settles, sets X-PAYMENT-RESPONSE, then calls next(). |
honoPaywall(opts: PaywallOptions) |
Same semantics as paywall, for a Hono Context. |
PaywallEngine |
The framework-agnostic engine both adapters wrap — requirements(resource), authorize(headerValue, resource), settle(payload, requirements), settlementHeader(result). Use it directly to build an adapter for another framework. |
PaywallOptions |
{ price, payTo, network?, description?, mimeType?, maxTimeoutSeconds?, resource?, facilitator? } (facilitator mode) or the same plus { wallet, account, reader } (self-settle mode) — one of the two settlement modes is required. |
getAddress |
Re-exported from viem for convenience. |
| Export | What it does |
|---|---|
Hood402Client |
The paying client. fetch(url, init?), fetchWithReceipt(url, init?), sign(requirements), spent(origin). |
Hood402ClientOptions |
{ signer, maxSpendPerOrigin?, allowedNetworks?, fetch?, validitySeconds?, now? }. |
fromAccount(account) |
Build a Signer from a viem LocalAccount (private key). |
fromWalletClient(client, address) |
Build a Signer from a viem WalletClient (e.g. an injected browser wallet). |
wrapFetch(opts) |
Convenience: returns a plain fetch-compatible function that transparently pays every 402 it hits. |
Signer |
The minimal typed-data-signing interface Hood402Client needs — { address, signTypedData }. |
PaidResponse |
{ response, paid, settlement? } — the return type of fetchWithReceipt. |
AuthorizationMessage |
The EIP-712 message shape (ExactEvmAuthorization with bigint numeric fields) passed to signTypedData. |
hood402 itself is a library and reads no env vars — these are the ones the examples,
facilitator, and scripts/verify-usdg.mjs in this repo use (see
.env.example). Wire them into your own app's config however you like.
| Variable | Used by | Meaning |
|---|---|---|
FACILITATOR_PRIVATE_KEY |
facilitator/ |
The gas wallet that broadcasts settlement transactions. Needs ETH on every network it settles. Never holds user funds — only relays signed EIP-3009 authorizations. |
FACILITATOR_NETWORKS |
facilitator/ |
Comma-separated networks to settle on (default robinhood,robinhood-testnet). |
ROBINHOOD_RPC_URL |
facilitator/ |
Optional mainnet RPC override (defaults to the public Robinhood Chain RPC). |
ROBINHOOD_TESTNET_RPC_URL |
facilitator/ |
Optional testnet RPC override. |
PORT |
facilitator/ |
HTTP port for the facilitator service (default 4021). |
LEDGER_PATH |
facilitator/ |
Path to the SQLite settlement ledger (default ./data/hood402-ledger.sqlite). |
ROBINHOOD_CHAIN_PRIVATE_KEY |
client examples | The wallet that signs USDG payments. Needs USDG balance, not ETH — the payer never pays gas. |
HOOD402_MAX_SPEND_PER_ORIGIN |
client examples | Hard per-origin spend cap in USDG (maps to Hood402ClientOptions.maxSpendPerOrigin). |
DEMO_PORT, DEMO_PAY_TO, FACILITATOR_URL |
examples/demo-server.ts |
Demo resource-server port, payee address, and facilitator URL. |
See facilitator/README.md for the
facilitator's own copy of this table with defaults spelled out per-field.
- The facilitator never holds user funds. It relays signed EIP-3009 authorizations — the payer's signature fixes the amount and recipient; the facilitator can only choose whether to broadcast, not what.
- Replay protection is two-layered. Every authorization carries a random 32-byte nonce
checked against on-chain
authorizationStatebefore verification passes, and the facilitator's SQLite ledger claims an idempotency slot on(network, payer, nonce)before broadcasting — a retried/settlefor the same signed payment returns the original transaction instead of double-spending gas. - Validity windows are short by default (
client.fetch's signer sets a 300-second window) — a leaked signature has a narrow blast radius. - The client enforces a hard spend cap per origin (
maxSpendPerOrigin) — it refuses to sign a payment that would exceed the cap, before any network call. - Keys are env vars only.
FACILITATOR_PRIVATE_KEY(the gas wallet) andROBINHOOD_CHAIN_PRIVATE_KEY(a payer) are never hardcoded or logged. See.env.example.
npm install
npm run build
npm test # vitest — the exact/EIP-3009 state machine, 65 tests
npm run verify:usdg # live on-chain proof of the E
8000
IP-3009 facet + domain separator
npm run e2e # full interop proof — see examples/e2e.ts
cd facilitator && npm install && npm test # 18 more tests: ledger idempotency + HTTP endpointsnpm run e2e proves the whole flow against live chain state: real mainnet reads, a real
HTTP 402 → sign → pay round trip over an actual socket, a real testnet RPC verify call, and
a real eth_call simulation of the settlement transaction. It states plainly which parts
are live broadcast vs. simulation — see the script's own header comment for the full
rationale.
All rights reserved. See LICENSE.
If robinhood-chain-x402 saves you time, star it on GitHub. Stars are how other developers and AI agents find the repositories worth trusting, and they cost you one click.
Know someone who would use it? Post on X · Share on Bluesky · Share on LinkedIn · Submit to Hacker News · Share on Reddit
Coding agents and LLM tooling can read this repo directly: AGENTS.md, llms.txt, llms-full.txt. Point an agent at https://github.com/nirholas/robinhood-chain-x402 and it has the context it needs.
- All repositories by nirholas: the full catalog, grouped by topic
- three.ws: the platform for 3D AI agents with Solana wallets, a skill marketplace and x402 payments
- Questions or ideas: open an issue or start a discussion