Agents

Give your agent
a route.

One MCP endpoint. Quote a swap or a whole basket, get an unsigned transaction back, pay per call in USDG. Nothing is ever sent for your agent.

  • No custodyExecute tools return a transaction to sign.
  • No accountA signed USDG payment is the only key.
  • No gas to payPayments are EIP-3009 signatures.

One paid call, start to finish

x402 —
  1. POST /x402/v1/baskets/quote{ "side": "buy", "token": "USDG", "id": "0x…", "amountUsd": 5 }
  2. 402 Payment RequiredPAYMENT-REQUIRED · scheme exact · eip155:4663 · the price, in USDG base units
  3. sign TransferWithAuthorizationEIP-3009 · domain "Global Dollar" v1 · a signature, no gas, only this amount to this address
  4. 200 OKPAYMENT-RESPONSE · settled onchain · the quote, with a quoteId valid 15 s

Reading this server's payment settings…

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"].

List the tools
curl -s {{MCP}} \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

3 / Pay per call

Pay per call

Reading this server's payment settings…

  1. 1

    Ask

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

1 · the 402
curl -i '{{API}}/x402/v1/quote?tokenIn=ETH&tokenOut={{USDG}}&amount=1000000000000000000'
2 to 4 · sign and retry (TypeScript, viem)
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 x402ReturnsPrice
GET /x402/v1/quoteSwap quote—
POST /x402/v1/swapUnsigned swap transaction—
POST /x402/v1/baskets/quoteBasket quote, every leg—
POST /x402/v1/baskets/txUnsigned basket transaction—
POST /x402/v1/basketsPublish a signed basket—
Free, rate limitedReturns
GET /healthBlock, lag, pool counts, market state, contract addresses, x402 prices
GET /v1/agentThis door in JSON: MCP URL, tools, x402 settings
GET /v1/marketWhether 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/swapSwap quote, then its unsigned transaction
GET /v1/baskets?sort=&q=Published baskets and their index
GET /v1/baskets/templatesUnsigned starting baskets from pool depth
GET /v1/baskets/:idOne basket, with its curator's signature
POST /v1/baskets/holdingsWhat an address holds of a basket
POST /v1/baskets/quote · POST /v1/baskets/txBasket quote, then its unsigned transaction
POST /v1/baskets/prepare · POST /v1/basketsEIP-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.