Skip to contentx402 is live on Robinhood Chain: agents can now pay for any model, per call, in USDG →
OmniRail

Trade

Let an agent swap tokens on Robinhood Chain with its own wallet. OmniRail finds the route, checks the price and builds the transaction; the agent signs it and pays gas. OmniRail never holds funds or keys.

Overview

  1. POST /trade/quote with the agent's wallet as taker: best route over verified Uniswap pools, price, impact, the least you receive (or the most you pay), the OmniRail fee, gas, and the steps to sign.
  2. First trade of a token only: send the approve transaction (ERC-20 → Permit2), sign the Permit2 typed data (no gas), then quote again with it.
  3. Sign tx exactly as returned, before expires_at (about 30 s).
  4. POST /trade/submit with signed_tx (we check it matches the quote byte for byte and broadcast it) or with tx_hash if you broadcast it yourself.
  5. GET /trade/{id}: pending, confirmed with the amounts actually received, or failed.

The agent pays its own gas in ETH: a quote is refused with insufficient_eth_for_gas when the wallet cannot cover it. Use a dedicated wallet for the agent, never your main one. Nothing here is investment advice.

Quickstart (viem)

import { createWalletClient, http, publicActions, defineChain } from "viem";
import { privateKeyToAccount } from "viem/accounts";

const robinhood = defineChain({ id: 4663, name: "Robinhood Chain", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
  rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } } });
const account = privateKeyToAccount(process.env.AGENT_WALLET_KEY);       // the agent's own, dedicated wallet
const wallet = createWalletClient({ account, chain: robinhood, transport: http() }).extend(publicActions);
const api = (path, body) => fetch("https://omnirail.org/api/v1" + path, {
  method: body ? "POST" : "GET",
  headers: { Authorization: "Bearer " + process.env.OMNIRAIL_KEY, "Content-Type": "application/json" },
  body: body && JSON.stringify(body),
}).then(async (r) => { const j = await r.json(); if (!r.ok) throw new Error(j.error.message); return j; });

const order = { taker: account.address, sell_token: "USDG", buy_token: "ETH", sell_amount: "25", slippage_bps: 50 };
let q = await api("/trade/quote", order);

// First trade of a token: approve Permit2 (a transaction), then sign the Permit2 typed data (free) and quote again.
let permit2;
for (const step of q.steps) {
  if (step.type === "approve") {
    const hash = await wallet.sendTransaction({ to: step.tx.to, data: step.tx.data });
    await wallet.waitForTransactionReceipt({ hash });
  }
  if (step.type === "permit2_signature") {
    const t = step.typed_data;
    const message = { ...t.message, details: { ...t.message.details, amount: BigInt(t.message.details.amount) }, sigDeadline: BigInt(t.message.sigDeadline) };
    const signature = await wallet.signTypedData({ domain: t.domain, types: t.types, primaryType: t.primaryType, message });
    permit2 = { signature, details: t.message.details, spender: t.message.spender, sig_deadline: t.message.sigDeadline };
  }
}
if (q.status === "needs_allowance") q = await api("/trade/quote", { ...order, permit2 });
if (q.status !== "ready") throw new Error(q.message ?? "quote not ready: " + q.status);

// Sign exactly what was quoted, then hand it over (or send it yourself and submit { quote_id, tx_hash }).
const request = await wallet.prepareTransactionRequest({ to: q.tx.to, data: q.tx.data, value: BigInt(q.tx.value), gas: BigInt(q.tx.gas) });
const signed_tx = await wallet.signTransaction(request);
await api("/trade/submit", { quote_id: q.id, signed_tx });

let t;
do { await new Promise((r) => setTimeout(r, 1500)); t = await api("/trade/" + q.id); } while (t.status === "pending");
console.log(t.status, t.result?.bought, t.explorer_url);

With buy_amount instead of sell_amount you receive exactly that amount and pay at most max_sell; the unused input is refunded in the same transaction.

Tokens & pools

TokenAddressDecimals
ETH0x000000000000000000000000000000000000000018
WETH0x0bd7d308f8e1639fab988df18a8011f41eacad7318
USDG0x5fc5360d0400a0fd4f2af552add042d716f1d1686
RAIL0xa4ddf100e3e0d3a9dd897e89d025622b794f0c0818

Routes use 3 Uniswap v3 and 6 v4 pools that OmniRail verified on-chain (WETH/USDG, native ETH/USDG and the $RAIL/ETH pool), directly or through ETH or USDG, and mix v3 and v4 legs when that pays more. Every contract (Universal Router, Permit2, quoters, tokens) is checked against a pinned code hash before any quote; pools with unreviewed hooks are left out. ETH and WETH are the same asset: wrap or unwrap them with the WETH contract. Buying an exact amount of $RAIL is not possible (its pool's hook only takes exact input): sell an exact amount instead.

Fees & pricing

  • Each quote costs $0.0020 from your balance (or x402); submit and status are free. Market-data calls cost $0.0005 each. Refused calls are not charged.
  • The OmniRail fee is taken inside the swap, in the token you buy: 0.25% (0.1% when $RAIL is either side). The quote shows it, and buy.amount / min_buy are already net of it.
  • Pool fees (and the $RAIL pool hook's ~3%) are part of the price, shown as pool_fee_bps; price_impact_bps is the move beyond them.
  • Gas is paid by the taker in ETH: gas.estimate, gas.limit and gas.max_cost_eth are in the quote.

Guards & limits

Slippagedefault 50 bps, at most 500 bps
Trade size$1 to $1,000 per trade, $5,000 per paying wallet per UTC day
Price impactat most 3% beyond the pools' own fees
Stable priceevery pool on the route within 5% of its 30-minute average
Simulationthe exact transaction is simulated from the taker before it is returned
Expirya signed transaction can be submitted for 30 s; one you already sent can be registered by tx_hash until the router refuses it, 150 s after the quote

Submit refuses anything that is not exactly the quoted transaction: another chain, another signer than taker, another destination, value or calldata, an expired quote, or a quote already submitted with another transaction (resubmitting the same one is idempotent).

Key limits & approval

Give each trading agent its own key with trade limits, set in the dashboard or via the account API. A request can only narrow them, never widen them.

"trade_limits": {
  "tokens": ["ETH", "USDG"],       // both sides must be listed
  "max_trade_usd": 50,
  "max_daily_usd": 200,            // every swap transaction handed out, UTC day
  "max_slippage_bps": 100,
  "approval_above_usd": 20         // above this, a human approves each trade
}

Above approval_above_usd, the quote answers status: "needs_approval" with an approval_url and no transaction. The wallet owner opens it, signs in and approves the exact terms (worst case included); the agent then quotes again with the same parameters and approval_id. Approvals expire after 15 minutes and work once. A price that moved since the approval does not cancel it: the trade keeps the approved worst case as its limit (min_buy, or max_sell), so it can only execute on the approved terms or better; if the price moved past that worst case, the quote answers approval_terms_changed and the agent must ask again.

Daily totals (the key's max_daily_usd and the $5,000 per paying wallet) count every swap transaction a quote hands out, from the moment the quote is ready, whether it is then submitted or sent directly: a transaction sent without POST /trade/submit is still found on-chain and counted. A quote whose transaction is never sent stops counting about 165 s after it was issued, once its deadline has passed.

Market data

curl "https://omnirail.org/api/v1/markets/price?base=ETH&quote=USDG" -H "Authorization: Bearer $OMNIRAIL_KEY"
# → { "price": "2500.72", "twap_price": "2499.41", "twap_seconds": 1800, "deviation_bps": 6, "pools": [ … ] }

curl "https://omnirail.org/api/v1/markets/candles?base=ETH&quote=USDG&interval=5m&limit=60" -H "Authorization: Bearer $OMNIRAIL_KEY"
curl "https://omnirail.org/api/v1/markets/tokens" -H "Authorization: Bearer $OMNIRAIL_KEY"
curl "https://omnirail.org/api/v1/wallets/0xYourAgentWallet/balances" -H "Authorization: Bearer $OMNIRAIL_KEY"

Candles come from the Swap logs of the pair's deepest direct pool: intervals 1m, 5m, 15m, 1h, 4h, at most 200 candles and 24 hours per request.

MCP tools

market_tokens, market_price, wallet_balances, trade_quote, trade_submit, trade_status. The agent signs with its own wallet outside OmniRail (for example with viem, as above) and passes the signed transaction to trade_submit.

Errors

StatusCodeMeaning
400unsupported_token, same_asset, invalid_permit, wrong_chain, wrong_signer, wrong_target, value_mismatch, calldata_mismatchBad request, or a submitted transaction that is not the quoted one.
403key_scope, approval_pending, approval_denied, approval_expiredThe key may not do this trade, or a human approval is missing.
409already_submitted, quote_not_ready, approval_used, approval_terms_changed, permit_too_smallQuote again.
410quote_expiredQuote again and sign the new transaction.
422no_route, trade_too_small, trade_too_large, price_impact_too_high, price_unstable, daily_limit, insufficient_balance, insufficient_eth_for_gas, simulation_failed, broadcast_failedRefused by a guard; nothing was charged or sent.
503trade_unavailable, trade_unverified, price_unverifiableChain data unavailable, or a contract failed verification: trading pauses. Retry later.