Trading
How a trade is planned, what comes back, how to sign and send it, and what each refusal means. The SDKs do all of this for you; read this to know what they check, or to call the API from another language.
The calls#
| Call | Use it to | Signable? |
|---|---|---|
POST /api/v1/live/prepare | plan a trade: quote, policy, the unsigned transactions and a simulation of exactly those, in one call. Use this. | yes |
POST /api/v1/live/quote | compare routes. Read-only; never refuses for risk. | no |
POST /api/v1/live/build | the same plan as prepare, without the simulation | yes |
POST /api/v1/live/simulate | a dry run against live state, for your wallet or a synthetic one | for your wallet only |
POST /api/v1/live/submit | relay transactions Orblivion built for you, once signed | |
POST /api/v1/live/fast | fast mode: a major-pair trade your SDK built and signed, checked and relayed in one call | you sign it |
POST /api/v1/live/revoke | take your token approvals back | yes |
prepare makes two round trips to the chain, about 0.1 s in all. Every field is in the API reference.
Plan a trade#
POST /api/v1/live/prepare
Authorization: Bearer pit_w_...
Content-Type: application/json
{
"wallet": "0xYourAgentWallet",
"token_in": "ETH",
"token_out": "USDG",
"notional_usd": 5
}walletis required and must be your key's wallet.token_in,token_out: a core symbol (ETH,WETH,USDG,cbBTC,ORBIO,NVDA,SPY) or any token's contract address.- The size:
notional_usd(dollars, at Orblivion's marks) oramount_in(atoms of the input, as a string). From $1 to $10,000 a trade (MIN_NOTIONAL,MAX_NOTIONAL). - Optional:
slippage_bps,min_amount_out,accept_risks,shieldandpolicies,max_fee_bps(the most the trade may pay; see Fees and gas), the approval and permit settings below,route_id(a route from an earlier quote),owner_attests_non_us(stock tokens).
The answer (abridged):
{
"ready": true,
"execution": "router",
"wallet": "0xyouragentwallet",
"token_in": "ETH", "token_out": "USDG",
"amount_in": "1835200000000000",
"notional_usd": 5,
"route": {"id": "uni:v3:100:0bd7-5fc5", "label": "Uniswap v3 0.01%", "venue": "uniswap", "router": "0x204faca1764b154221e35c0d20abb3c525710498"},
"quote": {"amount_out": "4996120", "vs_mid_bps": 1.6, "gas": 152300, "gas_usd": 0.0084},
"amount_out_min": "4971140",
"amount_out_expected": "4983630",
"deadline": 1791168497,
"fee": {"bps": 25, "side": "input", "recipient": "0x...", "token": "0x0000000000000000000000000000000000000000", "command": "TRANSFER", "amount": "4588000000000"},
"steps": [{"kind": "swap", "why": "Uniswap v3 0.01%: ETH -> USDG", "tx": {"...": "..."}, "decoded": {"...": "..."}}],
"tx": {"to": "0x204faca1764b154221e35c0d20abb3c525710498", "data": "0x3593564c...", "value": 1835200000000000, "chainId": 4663, "type": 2, "gas": 272359, "maxFeePerGas": 40168000, "maxPriorityFeePerGas": 0},
"simulation": {"ok": true, "amount_out": "4983630", "fee_transfer": {"amount": "4588000000000", "exact": true}, "gas_total": 209507, "funded_by_overrides": false},
"checks": [{"code": "MAX_SLIPPAGE", "ok": true, "detail": "50 bps of 300 max"}],
"approval": null,
"permit": null,
"risks": [],
"fee_bps": 25,
"fee_class": "major",
"fee_tier": 25,
"fee_side": "input",
"fee_token": "0x0000000000000000000000000000000000000000",
"shield": false,
"shield_available": true,
"timings": {"quote_ms": 50.1, "simulate_ms": 53.5, "total_ms": 106.5}
}readyis true whentxcan be signed and sent as it is. It is false while a Permit2 signature is needed first, or when the simulation failed.txis the transaction to sign: an unsigned EIP-1559 transaction with a zero tip,maxFeePerGasat twice the base fee, and a gas limit 30% over the simulated gas.stepslists every transaction or signature the trade needs, in order, each with what it is for (why) and Orblivion's decoding of it (decoded).txis the last step's.simulationran exactly the transactions instepsagainst the latest block.funded_by_overrides: truemeans your wallet doesn't hold the input yet, so the simulation lent it the balance: a dry run, which the SDKs refuse to send.checksare the policy checks that passed. A check that fails refuses the plan with its code.risks,fee_bps,fee_class,fee_tier,fee_side,fee_token,shieldare always at the top level. ETH to USDG is a major trade: 0.25% here, less at higher volume, paid in ETH from the input (ETH ranks above USDG: see which token pays the fee). See Tokens and risks and Fees and gas.
Execution shapes#
Every plan says how it executes in execution:
execution | When | What you sign |
|---|---|---|
router | almost every trade: Uniswap v2, v3 or v4 pools, graduated agent tokens included | one call to the Universal Router, execute(bytes,bytes[],uint256). Orblivion's fee is one command inside it: a fixed TRANSFER from the input, or a PAY_PORTION of the output. |
curve | an Orbio agent token still on its Pons bonding curve, from a plain wallet | two transactions: the fee transfer, then the curve's buy or sell paying your wallet (plus an exact approval to the curve when the input is a token). See Two-step curve trades. |
batch | the same curve trade, from a wallet delegated to MetaMask's EIP7702StatelessDeleGator | one transaction from your wallet to itself that runs the fee transfer, the approval and the curve call atomically. See Batches. |
Router plans pass through at most three hops, and only through WETH (or ETH), USDG and ORBIO. A plan names a v4 hook only when Orblivion knows it or disclosed it (see hooks).
Approvals and Permit2#
Native ETH needs no approval. A token input reaches the router through Permit2, which needs two things:
- An ERC-20 approval to Permit2, sent as its own transaction when what's left doesn't cover the trade. Its size follows
approval_mode:
approval_mode | The approval | A new one is needed |
|---|---|---|
budget (default) | approval_budget_usd worth (default $25, at most $10,000), never less than the trade | when the budget left doesn't cover the next trade |
exact | exactly this trade | every trade (one more transaction, about $0.002 of gas) |
unlimited | the maximum | never. Only with allow_unlimited_approval: true, otherwise UNLIMITED_APPROVAL. |
- A Permit2 allowance for the router, signed off-chain (EIP-712) and carried inside the swap. By default it is exactly this trade's amount, for 30 minutes (
permit_scope: "trade");"30d"allows the router up to the ERC-20 approval left for 30 days, so you sign once a month. A wallet that can't sign typed data can usepermit_mode: "transaction", a separatePermit2.approvetransaction instead.
Every plan from a token input carries approval: the mode, the budget, the allowance left, whether this trade needs a new approval and why (first, budget_exhausted or none), and what is left after the trade.
The permit round trip#
When the swap needs a Permit2 signature, the first prepare answers ready: false with what to sign:
{
"ready": false,
"permit": {"typed_data": {"domain": {"name": "Permit2", "chainId": 4663, "verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"}, "...": "..."}, "digest": "0x...", "then": "sign typed_data with the wallet and send the same body again with permit_signature and this permit_now"},
"permit_now": 1791168377,
"steps": [{"kind": "approve", "...": "..."}, {"kind": "sign_permit", "...": "..."}, {"kind": "swap", "needs_signature_first": true, "...": "..."}]
}- If
stepsstarts with anapprove, check it (the token is your input, the spender is Permit2, the amount is what you expect for your mode), sign it and send it first. - Check the typed data: domain Permit2 on chain 4663, the token is your input, the spender is the Universal Router, the amount is exactly this trade's, and the expiry is near. Rebuild the digest yourself and compare it with
digest. Then sign it. - Send the same body again with
permit_signatureandpermit_now. The answer isready: true, with the swap carrying your permit, simulated.
Taking approvals back#
POST /api/v1/live/revoke with your wallet (and tokens, default ["USDG"]) returns the transactions that undo them: approve(Permit2, 0) while an allowance is left, and Permit2.lockdown while the router holds a live Permit2 allowance. They are simulated against your wallet first; nothing_to_revoke: true means there is nothing left to undo. Send them like any other step.
Slippage and the minimum output#
Every trade carries an on-chain minimum: the transaction reverts rather than deliver less. Orblivion sets it from your slippage limit:
slippage_bpscounts from the quote before Orblivion's fee. Without it, each pair has its own default for the size: the tokens' measured tolerance plus the fee (limits.slippage_tolerance_by_sizeinGET /api/v1/config).- Each pair has a cap, and no request may go above it (
MAX_SLIPPAGE); the policy's ceiling is 300 bps. amount_out_minis the quote less your slippage, after the fee. It is what the calldata enforces.min_amount_outin the request is a floor of your own, in output atoms, such as a limit price. When no route can meet it now the plan is refused withLIMIT_NOT_REACHABLE, before anything is built.- The route itself is held to an independent price: the pair's reference pool mid for core pairs (
ROUTE_VS_MID), and for other tokens a Chainlink feed, a time-weighted pool average or a median of distinct pools, where one exists (ROUTE_VS_REF). This catches a pool someone pushed just before your trade. - The deadline is 120 seconds from the build. A swap signed and left unsent expires.
The SDKs check all of this again before signing: the minimum must be at most your slippage under the quote and the simulation, and within 3% of the input's value at Chainlink prices they read themselves (MIN_OUT, QUOTE, PRICE). For a token without a feed they quote the exact route themselves over their own RPC (QUOTE_OWN).
Batches#
Orblivion builds one kind of batch: an EIP-7702 batch for a wallet whose code is MetaMask's EIP7702StatelessDeleGator v1.3.0. The wallet sends one transaction to itself, execute(bytes32 mode, bytes executionCalldata), in batch mode with the default exec type, so the calls run in order and all of them revert if one does:
- Orblivion's fee: a token
transferto the fee recipient, or an ETH transfer; - for a token input to a curve,
approve(curve, exactly the amount traded); - the trade: the curve's
buyorsellpaying the wallet, or a Universal Router call (with noPAY_PORTION, since the batch's transfer is the fee).
That is the order when the fee comes from the input, as on every curve buy. A curve sale pays its fee in the pair (ORBIO or ETH, which outrank the token), so its batch runs approve(curve, the whole input), then the sell, then the fee: a transfer of exactly floor(min_out × bps / 10,000) of the pair, where min_out is the sale's guaranteed minimum. See which token pays the fee.
The SDKs read your wallet's code themselves and accept a batch only in exactly that shape (BATCH_* rules). A plain wallet gets the two-step form below instead. curve_fee_mode (auto, batch, two_step) picks one: auto uses the batch when the wallet is delegated, and batch from a plain wallet is refused (FEE_MODE).
Two-step curve trades#
An Orbio agent token trades on its Pons bonding curve until it graduates, and only against its pair token (usually ORBIO; CURVE_PAIR otherwise). The curve takes no fee parameter, so from a plain wallet the trade is two transactions:
fee_transfer: Orblivion's fee,floor(amount_in × bps / 10,000)of the input, to the fee recipient (always the input here, a sale's token included, so the fee can't be skipped:fee_side: "input");swap: the curve'sbuy(amount, minOut, yourWallet)orsell(amount, minOut, yourWallet)with the rest.
Send them together in one submit, as {"raw_txs": [<approve>, <fee_transfer>, <swap>]} in order, signed with consecutive nonces. Orblivion sends the approval and the fee, waits up to 20 seconds for the fee to be mined, and sends the swap only once the fee has succeeded. It never relays a fee on its own (FEE_FIRST). Each step carries valid_until, its build plus 120 seconds; an expired plan is refused.
Curves also refuse two things Orblivion won't build: a buy in a token's first seconds, when the curve charges a snipe tax (SNIPE_TAX: try again a few seconds later), and a buy that would take the curve to graduation and be only partly filled (CURVE_GRADUATED: buy less, or trade the pool once it has graduated).
Relay and submit#
POST /api/v1/live/submit
Authorization: Bearer pit_w_...
Content-Type: application/json
{"raw_tx": "0x02f9..."}Orblivion relays a transaction only if it is as built: its to, value and calldata equal a step of a plan Orblivion built for the same API key in the last 30 minutes, and it is signed by the wallet that plan was for (or, for a session trade, its executor). Anything else is NOT_AS_BUILT, with the fields that differ in diffs. The check is one lookup, about a millisecond; the relay keeps a warm connection to the sequencer.
{
"tx_hash": "0x...",
"as_built": {"kind": "swap", "wallet": "0xyouragentwallet", "...": "..."},
"timings": {"check_ms": 0.7, "relay_ms": 238.4, "total_ms": 239.1}
}- A server can run with relaying off;
submitthen answers403 SUBMIT_DISABLEDand you send the signed transaction through any Robinhood Chain RPC yourself.submit_enabledinGET /api/v1/configsays which. 502 SEQUENCER_UNREACHABLEmeans the sequencer didn't answer. The transaction may or may not have arrived: look for its receipt before you sign anything else with the same nonce.422 SUBMIT_REJECTEDmeans the sequencer, or Orblivion's own decoding at the relay, refused it.
Transactions are public as soon as they are sequenced. See Security model.
Receipts#
Receipts come from the chain: poll any Robinhood Chain RPC with eth_getTransactionReceipt and the tx_hash that submit returned. With blocks every tenth of a second, a receipt is usually there within a second of sending. The SDKs poll for it themselves (several looks in flight, every 100 ms), then check what was mined against what they signed.
Fast mode#
For an agent that holds its own key and trades a major pair, fast mode cuts the path from decision to relay to one call. The SDK keeps what a trade needs warm in the background (its own Chainlink marks over your RPC, the wallet's nonce, the base fee, the input's allowances, and your fee tier from GET /api/v1/live/fast/terms), builds the transaction itself on a route both sides pin, signs it, and sends it to POST /api/v1/live/fast. Orblivion rebuilds the calldata with its own builder and relays only an exact match.
client = OrblivionClient(account=Account.from_key(key))
fast = client.fast().start() # keeps marks, nonce, gas, allowances and your fee tier warm
r = fast.trade("ETH", "USDG", amount_usd=10)
print(r.path, r.tx_hash, r.timings["decision_to_accepted_ms"])const client = createOrblivionClient({ account: privateKeyToAccount(key) })
const fast = await client.fast().start()
const r = await fast.trade({ sell: 'ETH', buy: 'USDG', amountUsd: 10 })
console.log(r.path, r.txHash, r.timings.decisionToAcceptedMs)- Pairs: ETH, WETH, USDG, NVDA and SPY, both ways where a deep pool without a hook exists (the terms list them). Stock tokens only while the US stock-token session is open. cbBTC, ORBIO, Orbio agents, Pons curves and long-tail tokens are never fast: they go through
prepare(your SDK takes the normal path for them before it signs anything). - Each route has a size cap (
routes.max_usdin the terms): the largest size, at Orblivion's marks, at which its pinned pool was measured to stay within 10 bps (at $1,000) or 25 bps (above) of the best routepreparewould take. A larger trade takes the normal path before your SDK signs anything (ROUTE_CAP; Orblivion refuses it too). - The minimum comes from your SDK's own quote of the pinned route (kept warm, at most 3 s old, scaled to the size) less the tolerance, at most Orblivion's default for the pair, as
preparesets it from its own quote. The pool must trade within the pair's band of the Chainlink marks beyond its own fee: 75 bps for a pair against USDG, 125 for a cross pair such as ETH and SPY. Further under, your SDK takes the normal path (POOL_FAR), and Orblivion refuses the pool's simulated delivery too. Orblivion also refuses a minimum more than the pair's cap under what the trade delivers now (MIN_OUT, so a stale quote can't sell you cheap), any minimum too far under its own marks, and, on NVDA and SPY pairs, a delivery too far under the reference pools' mid (ROUTE_VS_MID). - The size is checked twice: your SDK holds its RPC's Chainlink marks to Orblivion's before it sizes a trade (
MARKS_DIFFERtakes the normal path), and the request carries the USD you asked for and your client's notional cap, which Orblivion holds the signed amount to at its own marks (AMOUNT_USD,CLIENT_CAP). A wrong or lying RPC can't turn $100 into $10,000. - The fee is your wallet's major tier (and Shield's add-on), in ETH, WETH or USDG by the fee currency rule, to Orblivion's pinned recipient: exactly what
preparewould build. - Falling back: anything stale, an ineligible pair, or a first trade from a token you haven't approved to Permit2 yet, and the SDK takes the normal path before it signs anything (
r.path == "fallback"). Passfallback=Falseto get the error instead. - One trade, never two. Once a fast transaction is signed, the SDK signs another only after a refusal Orblivion answers before relaying, and only after asking your own RPC about the first: if the chain has it, that is the trade (
r.notesays so); if your wallet's nonce has moved past it, the next transaction takes a fresh nonce; otherwise the retry or the normal path's first transaction reuses the same nonce, so at most one of them can land. A refusal that may follow a relay (NONCEwithout that proof,IN_FLIGHT,SEQUENCER_UNREACHABLE,SUBMIT_REJECTED, or no answer at all) raises with the hash: look it up before trading again. simulate=Falseskips the server's simulation of the exact signed transaction (one round trip to the node, about 50 to 220 ms). Every other check still runs, the wallet must hold what the transaction spends, and such relays are limited to one a second per key; a transaction that would revert is then relayed and costs its gas.- Dry run:
dry_run=Truesendscheck_only: Orblivion runs every check and the simulation, and relays nothing.
Where to host your agent#
Every trade ends with a round trip to Robinhood Chain's sequencer, and fast mode leaves little else. Run your agent in US East, close to the sequencer and to Orblivion's servers, and use an RPC near it. Measured on 7 Oct 2026 from a development machine far from US East: the sequencer was 246 ms away on a kept-alive connection (241 ms to open one), and the two public RPCs the SDKs use by default 49 ms and 211 ms. From there each of those is paid on every trade; from US East they should be far shorter, which wasn't measured.
Errors#
Every refusal is JSON with a stable code. The common ones while trading:
| Code | Status | What to do |
|---|---|---|
RISK_NOT_ACCEPTED | 422 | the trade carries severe risks you didn't accept; they are in unaccepted. Read them, then accept them by name or don't trade. |
RISK_BLOCKED | 422 | a Dossier Shield policy refused it (blocked, policy). |
NO_ROUTE | 422 | no venue Orblivion can build a trade through. |
MAX_SLIPPAGE, MAX_NOTIONAL, MIN_NOTIONAL | 422 | the request is outside the pair's slippage cap or the size limits. |
LIMIT_NOT_REACHABLE | 422 | your min_amount_out can't be met now. |
ROUTE_VS_MID, ROUTE_VS_REF | 422 | the route's price is too far from the reference; try again, or a smaller size. |
FEE_FIXED | 422 | the request named a fee; the server sets it from the trade's class, the wallet's tier and Shield. Cap it with max_fee_bps instead. |
FEE_BPS | 422 | the plan's fee would be above your max_fee_bps. Raise the cap, or don't trade. |
WALLET_MISMATCH | 403 | the wallet isn't your key's. |
NOT_AS_BUILT | 422 | the signed transaction isn't one Orblivion built for this key. |
TOKEN_CHANGED | 409 | the token's proxy now runs other code than when it was screened; it is screened again, so retry shortly. |
RATE_LIMITED | 429 | wait for Retry-After seconds. |
The full list, with every status, is on Errors.