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
POST /trade/quotewith the agent's wallet astaker: best route over verified Uniswap pools, price, impact, the least you receive (or the most you pay), the OmniRail fee, gas, and thestepsto sign.- First trade of a token only: send the
approvetransaction (ERC-20 → Permit2), sign the Permit2 typed data (no gas), then quote again with it. - Sign
txexactly as returned, beforeexpires_at(about 30 s). POST /trade/submitwithsigned_tx(we check it matches the quote byte for byte and broadcast it) or withtx_hashif you broadcast it yourself.GET /trade/{id}:pending,confirmedwith the amounts actually received, orfailed.
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
| Token | Address | Decimals |
|---|---|---|
| ETH | 0x0000000000000000000000000000000000000000 | 18 |
| WETH | 0x0bd7d308f8e1639fab988df18a8011f41eacad73 | 18 |
| USDG | 0x5fc5360d0400a0fd4f2af552add042d716f1d168 | 6 |
| RAIL | 0xa4ddf100e3e0d3a9dd897e89d025622b794f0c08 | 18 |
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_buyare 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_bpsis the move beyond them. - Gas is paid by the taker in ETH:
gas.estimate,gas.limitandgas.max_cost_ethare in the quote.
Guards & limits
| Slippage | default 50 bps, at most 500 bps |
| Trade size | $1 to $1,000 per trade, $5,000 per paying wallet per UTC day |
| Price impact | at most 3% beyond the pools' own fees |
| Stable price | every pool on the route within 5% of its 30-minute average |
| Simulation | the exact transaction is simulated from the taker before it is returned |
| Expiry | a 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"e=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"e=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
| Status | Code | Meaning |
|---|---|---|
| 400 | unsupported_token, same_asset, invalid_permit, wrong_chain, wrong_signer, wrong_target, value_mismatch, calldata_mismatch | Bad request, or a submitted transaction that is not the quoted one. |
| 403 | key_scope, approval_pending, approval_denied, approval_expired | The key may not do this trade, or a human approval is missing. |
| 409 | already_submitted, quote_not_ready, approval_used, approval_terms_changed, permit_too_small | Quote again. |
| 410 | quote_expired | Quote again and sign the new transaction. |
| 422 | no_route, trade_too_small, trade_too_large, price_impact_too_high, price_unstable, daily_limit, insufficient_balance, insufficient_eth_for_gas, simulation_failed, broadcast_failed | Refused by a guard; nothing was charged or sent. |
| 503 | trade_unavailable, trade_unverified, price_unverifiable | Chain data unavailable, or a contract failed verification: trading pauses. Retry later. |