402 Payment RequiredPAYMENT-REQUIRED · scheme exact · eip155:4663 · the price, in USDG base units
sign TransferWithAuthorizationEIP-3009 · domain "Global Dollar" v1 · a signature, no gas, only this amount to this address
200 OKPAYMENT-RESPONSE · settled onchain · the quote, with a quoteId valid 15 s
Reading this server's payment settings…
Response
1Connect
Streamable HTTP, stateless, POST only. No session, no key.
MCP endpoint—
—
What it is
One router, callable.
This is the same engine behind the swap and baskets pages, exposed to software. An agent can search tokens, quote a swap or a whole basket across Uniswap V2, V3 and V4 pools on Robinhood Chain, and get back an unsigned transaction to sign with its own key. Reading is free. Quotes, transactions and publishing a basket are paid per call in USDG over x402 when the server has payments on.
Chain
Robinhood Chain · eip155:4663
Router
—
BasketRouter
—
Amounts
base units, as decimal strings
The contracts are not audited. Where an address above reads "Not deployed yet", quotes work and the execute calls answer 503.
2 / Call a tool
MCP tools
Read live from GET /v1/agent and tools/list on this server.
Loading the tool list…
Paying for a tool over MCP: a paid tool called without a payment answers isError: true with the payment requirements in structuredContent. Call it again with params._meta["x402/payment"] set to the payment object (the same JSON as the HTTP header, not base64). The receipt comes back in result._meta["x402/payment-response"].
Call a paid route with no payment. The answer is 402 with a PAYMENT-REQUIRED header (base64 JSON, x402 v2) and the same JSON as the body: price, asset, payTo, network.
2
Sign
Sign an EIP-3009 TransferWithAuthorization for exactly that amount of USDG to payTo, with a random 32-byte nonce and a validBefore at least 10 seconds ahead. It costs the payer no gas.
3
Retry
Send the same request with the header PAYMENT-SIGNATURE: base64 of { x402Version: 2, accepted, payload: { signature, authorization } }. Version 1 clients use X-PAYMENT.
4
Settle
The server checks the signature, runs the call, then settles the transfer and pays its gas. The receipt is in PAYMENT-RESPONSE. A call that fails is not charged, and a used authorisation gets a new 402.
import { privateKeyToAccount } from "viem/accounts";
const API = "{{API}}";
const USDG = "{{USDG}}";
const account = privateKeyToAccount(process.env.AGENT_KEY as `0x${string}`);
const url = `${API}/x402/v1/quote?tokenIn=ETH&tokenOut=${USDG}&amount=1000000000000000000`;
// 1. ask: the 402 names the price
const first = await fetch(url);
if (first.status !== 402) throw new Error(`no payment asked (HTTP ${first.status})`);
const offer = JSON.parse(Buffer.from(first.headers.get("payment-required")!, "base64").toString());
const req = offer.accepts[0]; // { scheme: "exact", network: "eip155:4663", amount, asset, payTo, extra }
// 2. sign an EIP-3009 authorisation for exactly that amount. No gas, no approval.
const authorization = {
from: account.address,
to: req.payTo,
value: req.amount,
validAfter: "0",
validBefore: String(Math.floor(Date.now() / 1000) + 90),
nonce: `0x${Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex")}`,
};
const signature = await account.signTypedData({
// USDG's own EIP-712 domain; the 402 repeats name and version in req.extra
domain: { name: "Global Dollar", version: "1", chainId: 4663, verifyingContract: USDG },
types: {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
},
primaryType: "TransferWithAuthorization",
message: {
from: authorization.from,
to: authorization.to,
value: BigInt(authorization.value),
validAfter: 0n,
validBefore: BigInt(authorization.validBefore),
nonce: authorization.nonce as `0x${string}`,
},
});
// 3. retry the same request with the payment
const payment = { x402Version: 2, accepted: req, payload: { signature, authorization } };
const paid = await fetch(url, { headers: { "PAYMENT-SIGNATURE": Buffer.from(JSON.stringify(payment)).toString("base64") } });
if (!paid.ok) throw new Error(`payment refused: ${(await paid.json()).error}`);
// 4. the quote, and the receipt of the USDG transfer
const quote = await paid.json();
const receipt = JSON.parse(Buffer.from(paid.headers.get("payment-response")!, "base64").toString());
console.log(quote.quoteId, quote.amountOut, receipt.transaction);
REST
HTTP routes
Base URL —. The paid routes answer 402 until paid; with payments off they answer directly.
Paid over x402
Returns
Price
GET /x402/v1/quote
Swap quote
—
POST /x402/v1/swap
Unsigned swap transaction
—
POST /x402/v1/baskets/quote
Basket quote, every leg
—
POST /x402/v1/baskets/tx
Unsigned basket transaction
—
POST /x402/v1/baskets
Publish a signed basket
—
Free, rate limited
Returns
GET /health
Block, lag, pool counts, market state, contract addresses, x402 prices
GET /v1/agent
This door in JSON: MCP URL, tools, x402 settings
GET /v1/market
Whether the US stock market is open, and when that changes
GET /v1/tokens?q=&class=
Tokens the router can trade
GET /v1/quote · POST /v1/swap
Swap quote, then its unsigned transaction
GET /v1/baskets?sort=&q=
Published baskets and their index
GET /v1/baskets/templates
Unsigned starting baskets from pool depth
GET /v1/baskets/:id
One basket, with its curator's signature
POST /v1/baskets/holdings
What an address holds of a basket
POST /v1/baskets/quote · POST /v1/baskets/tx
Basket quote, then its unsigned transaction
POST /v1/baskets/prepare · POST /v1/baskets
EIP-712 payload to sign, then publish
Custody and limits
What it can and cannot do
Custody
None. swap_execute, baskets_execute and their HTTP routes return an unsigned transaction, { to, data, value }, plus the ERC-20 approvals still missing. The agent signs and sends it. Nothing is ever sent for the agent.
Payments
A payment authorises one transfer of the exact price to this server's payTo address. The signature cannot move another amount or pay another address.
Per-call limit
Basket quotes on the agent door are refused above a per-call notional set by the server.
Slippage
MCP tools cap slippageBps at 2000 (20%). Defaults: 50 for a swap, 100 for each basket leg. The HTTP routes accept 0 to 5000.
Quotes
A quoteId lives about 15 seconds. Execute re-checks the routes onchain first and answers 409 when the price has moved past your slippage: quote again.
Deadline
deadlineSec defaults to 120 and may be at most 3600.
Stock tokens
Amounts are raw balances. For a stock token the ERC-8056 uiMultiplier is returned beside them: shares = balance × multiplier / 1e18. A quote that touches a stock token while the US market is closed carries a line in warnings.
Baskets
2 to 20 tokens, weights in basis points adding up to 10000. A basket published through this door is marked as curated by an agent. Its index is a pool-price index since publishing, not a return.
Transport
One JSON-RPC message per POST, or a batch of up to 20. No sessions and no server stream: GET /mcp answers 405.