OrdoFi Documentation
Protected execution infrastructure on Robinhood Chain and Arc. A protected JSON-RPC gateway, a sealed-bid second-price backrun auction with signed and anchored outcomes, trust-minimised on-chain settlement, a sequencer feed relay, and the measurement that makes the numbers checkable.
Overview#
Robinhood Chain is an Arbitrum Nitro rollup with a single, first-come-first-served sequencer and no public mempool. Blocks arrive every ~100 ms. That combination changes what MEV looks like: nobody can front-run a pending transaction, because there is no pending set to read. What exists instead is a latency race to backrun: the sequencer feed reveals each transaction the instant it is ordered, and bots compete to trade immediately after anything that moves a pool.
OrdoFi sits between the user and the sequencer and does three things with that fact:
Protect the submission
Every raw transaction is simulated from its recovered sender before it is forwarded. A transaction that would revert is refused and never pays gas. Nothing is broadcast to a third-party provider on the way; sends go to the sequencer operator's endpoint.
Auction the backrun
If a transaction touches a pool, the right to trade right after it is sold in a sealed-bid, second-price auction that closes in 200 ms. The user's transaction is dispatched first, then the winner's. The clearing price is charged on-chain and 90% returns as rebates.
Make it checkable
Bids are acknowledged and rounds are receipted under EIP-712 signatures from the auctioneer; receipts are Merkle-anchored to OrdoReceiptLog. An independent watcher measures backrun extraction across every venue and publishes it.
Architecture#
wallet / app ──eth_sendRawTransaction──▶ gateway rpc.ordofi.network :8547
│ simulate as sender (eth_call) · reject reverts
│ key in auction mode → POST /submit
▼
auction auction.ordofi.network :8548
│ hold ≤ 200 ms · hints via eth_simulateV1
│ ws /searcher ──▶ searchers (sealed bids, EIP-712)
│ second price · dispatch user tx, then backrun
│ settle(Settlement, sig) · sign receipt
▼
OrdoSettlement bond debit → user / app / protocol split
OrdoReceiptLog Merkle root of receipts, every 25 rounds
sequencer feed wss://feed.mainnet.chain.robinhood.com ──▶ relay ws /feed ──▶ searchers
chain ──▶ watcher (arb attribution, OHLC tape) ──▶ SQLite ──▶ app.ordofi.network/api
Three processes matter operationally: the gateway (stateless, keyed), the auction (holds the auction window, WebSocket fan-out, settlement and receipts) and the watcher (indexes every block into SQLite). The web app is a read layer over the index plus a small set of on-chain reads.
Network & public endpoints#
| Parameter | Value |
|---|---|
| Chain | Robinhood Chain: Arbitrum Nitro (Orbit) L2 settling to Ethereum mainnet |
| Chain id | 4663 · 0x1237 |
| Block time | ~0.1 s · single centralized sequencer, FCFS ordering, no public mempool |
| Gas token | ETH (18 decimals) |
| Sequencer RPC | https://rpc.mainnet.chain.robinhood.com |
| Sequencer feed | wss://feed.mainnet.chain.robinhood.com |
| Explorer | https://robinhoodchain.blockscout.com |
| WETH | 0x0bd7d308f8e1639fab988df18a8011f41eacad73 |
| USDG | 0x5fc5360d0400a0fd4f2af552add042d716f1d168 (6 decimals) |
| Uniswap V3 factory / QuoterV2 / SwapRouter02 | 0x1f7d7550b1b028f7571e69a784071f0205fd2efa · 0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7 · 0xcaf681a66d020601342297493863e78c959e5cb2 |
| Uniswap V4 PoolManager / PositionManager / StateView | 0x8366a39cc670b4001a1121b8f6a443a643e40951 · 0x58daec3116aae6d93017baaea7749052e8a04fa7 · 0xf3334192d15450cdd385c8b70e03f9a6bd9e673b, every V4 pool lives inside the singleton and is keyed by its 32-byte PoolId; native ETH is currency 0x0 |
| OrdoFi host | Service | Transport |
|---|---|---|
rpc.ordofi.network | Gateway (JSON-RPC) | HTTPS POST · GET / /health /metrics |
auction.ordofi.network | Auction, receipts, feed relay | HTTPS · WSS /searcher · WSS /feed |
app.ordofi.network | Terminal, explorer, portal, this reference, public API | HTTPS · /api/* is CORS-open |
Deployed contracts (chain 4663)#
| Contract | Address | Role |
|---|---|---|
| OrdoSettlement | 0xbC680922DaF2F65a8B957e5238857f8c68BeDabb | Searcher bonds, second-price debit against a signed maximum, rebate accounting |
| OrdoBundler | 0xc0bccFb3aA4ad9160d272645376a1797a32f3c4a | Per-owner CREATE2 executors for atomic multi-call bundles |
| OrdoReceiptLog | 0x89926c06cad403fDDD481C599b2ce709EBC936B9 | Merkle roots over all auction receipts ever issued |
| Auctioneer | 0x894255564bBb71585AebDf9337888b4b6E72C649 | Signs acks and receipts; the only address allowed to settle() and commit() |
| Protocol treasury | 0xf5ba571781533aaab9fc155311c93f5b1100affd | Receives the protocol share of each settlement |
| OrdoPriceFeeds | 0x7DD461f4a56422E419C7F2d0cF44d58600F860d8 | Registry of the stock price feeds: one Chainlink-style feed per stock, market hours |
| OrdoExecutor | 0x8E303737059890abC216180bAEcd87b2622EEd75 | Runs signed Ordo Execution orders: at least the user's minimum, at most once |
Deployment block 51544378. Split constants at deploy: appBps = 500, protocolBps = 500; the user share is the remainder (90%).
Quickstart#
Wallet user#
Add the network with rpc.ordofi.network as its RPC URL. No key is needed: the anonymous tier covers every method a wallet uses, including a revert-protected eth_sendRawTransaction. One click at rpc.ordofi.network, or programmatically:
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [{
chainId: "0x1237",
chainName: "Robinhood Chain · OrdoFi protected",
rpcUrls: ["https://rpc.ordofi.network"],
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
blockExplorerUrls: ["https://robinhoodchain.blockscout.com"],
}],
});
App or wallet developer#
Mint a key with a rebate address so your users' flow goes through the auction and the app share accrues to you. Then send exactly as before, with one header.
# 1. a key (returned once; only its SHA-256 is stored)
curl -X POST https://app.ordofi.network/api/keys \
-H 'content-type: application/json' \
-d '{"label":"my-wallet","rebateAddress":"0xYourTreasury"}'
# 2. route order flow through the auction
curl https://rpc.ordofi.network \
-H 'x-api-key: ordo_…' -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_sendRawTransaction","params":["0x02f8…"]}'
Searcher#
import { OrdoSearcher, OrdoBond } from "@ordofi/sdk";
const SETTLEMENT = "0xbC680922DaF2F65a8B957e5238857f8c68BeDabb";
await new OrdoBond(process.env.SEARCHER_KEY, SETTLEMENT).deposit("0.05"); // bond ≥ your largest bid
new OrdoSearcher({
auctionWsUrl: "wss://auction.ordofi.network/searcher",
privateKey: process.env.SEARCHER_KEY,
settlementAddress: SETTLEMENT,
onOpportunity: async (opp, { account }) => {
if (!opp.hint.poolsTouched.length) return null; // nothing to rebalance
const backrunRawTx = await buildBackrun(opp, account); // your strategy
return { maxBidWei: 2_000_000_000_000_000n, backrunRawTx }; // 0.002 ETH ceiling
},
}).connect();
Networks at a glance#
Ordo runs the same gateway on more than one chain. One API key works on all of them; usage and billing are per key, not per chain. Each network has its own hostname, its own node, and its own set of features.
| Network | Chain ID | RPC | WebSocket | Explorer | Gas | What Ordo offers |
|---|---|---|---|---|---|---|
| Robinhood Chain Robinhood's L2 (Arbitrum Nitro) | 4663 | https://rpc.ordofi.network | wss://rpc.ordofi.network | blockscout | ETH | Reads at the head, Private Send (simulate, revert and approval protection), tracing, Ordo Swap with best-route and split routing, order-flow auction with MEV rebates, live RPC comparison, validator watchtower |
| AevaChain mainnet beta An asset-native L1: dividends, pledges and settlement in the protocol | 2383 | https://aeva.ordofi.network | wss://aeva.ordofi.network | Aevascan | AEVA | Reads from our own archive node, protected send (revert and approval checks), tracing (debug_trace* back to genesis, which the network's own endpoint does not offer), WebSocket subscriptions (likewise). |
| AevaChain testnet | 2382 testnet | https://aeva-testnet.ordofi.network | wss://aeva-testnet.ordofi.network | Aeva explorer | AEVA (test) | The same as mainnet, on Aeva's testnet, from our own testnet archive node. |
| Arc testnet now · mainnet 16 Sep Circle's L1 for stablecoin finance | 5042 mainnet5042002 testnet | https://arc.ordofi.network | wss://arc.ordofi.network | arcscan | USDC | Reads at the head, Private Send (simulate, revert and approval protection), tracing, WebSocket subscriptions. Swap and auction to follow. |
Everything in the gateway reference below applies to every network unless a row above says otherwise; ordo_quoteSwap, ordo_sendPrivateTransaction (auction) and ordo_sendBundle are Robinhood Chain only for now. On Arc, eth_sendRawTransaction is simulated and then delivered to the network's RPC. On AevaChain it is checked with eth_call (the chain has no eth_simulateV1, so the check for payments to addresses nobody controls does not run there) and delivered to the network's endpoint, with our node as the fallback.
Gateway referenceAll networks#
This section applies to rpc.ordofi.network (Robinhood Chain), arc.ordofi.network (Arc) and aeva.ordofi.network (AevaChain) alike. Where a method exists on one network only, its row says so.
A JSON-RPC 2.0 endpoint on HTTPS POST /. Batches (a JSON array of requests) are accepted and answered as an array. Standard eth_* methods pass through to the chain; the methods below are intercepted. Non-POST requests to paths other than /, /health and /metrics* return a JSON 404. CORS is open (access-control-allow-origin: *, headers content-type, x-api-key, authorization).
Authentication & rate limits#
Keys travel in x-api-key or Authorization: Bearer <key>. A key carries a mode and an optional rebate address:
| Tier | Who | Methods | Limit | eth_sendRawTransaction behaviour |
|---|---|---|---|---|
| Anonymous | Wallets, anyone | Full wallet set (see below) + ordo_simulate | 3,000 upstream reads + 60 sends per minute per source IP | Protected direct send |
Key · direct | Apps without a rebate address | Everything | Per key, default 12,000/min | Protected direct send |
Key · auction | Apps with a rebate address | Everything | Per key, default 12,000/min | Auction via POST /submit; falls back to protected direct send if the auction is unreachable, and the fallback is counted |
Anonymous methods: eth_chainId net_version web3_clientVersion eth_blockNumber eth_syncing eth_gasPrice eth_maxPriorityFeePerGas eth_feeHistory eth_getBalance eth_getCode eth_getStorageAt eth_getTransactionCount eth_call eth_estimateGas eth_getLogs eth_getBlockByNumber eth_getBlockByHash eth_getBlockReceipts eth_getTransactionReceipt eth_getTransactionByHash eth_getTransactionByBlockNumberAndIndex eth_getTransactionByBlockHashAndIndex eth_getBlockTransactionCountByNumber eth_getBlockTransactionCountByHash eth_getUncleCountByBlockNumber eth_getUncleCountByBlockHash eth_getProof eth_createAccessList eth_accounts eth_protocolVersion net_listening eth_sendRawTransaction ordo_simulate, every standard eth_/net_ read plus the protected send. Without a key, debug_*, trace_*, filters, subscriptions, eth_simulateV1 and the ordo_ routing methods return -32601 saying so.
Limits are fixed 60-second windows. Exceeding one returns -32005 with retry in Ns in the message. Keys minted at the portal are stored as SHA-256 hashes; a lost key is re-issued, never recovered.
Plans & request units#
Keyed requests are metered in request units (RU) against a monthly budget (calendar month, UTC). Most methods are 1 RU; the ones that cost a node real work cost more. Plans are priced in USD and paid in ETH or USDG on Robinhood Chain: send to the treasury shown in the portal, paste the transaction hash, and the key is credited at the price of that moment. Plan fees and overage are deducted from that credit; a paid plan renews every 30 days while credit lasts and returns to Free when it does not. Nothing is billed to a card, ever. Live figures: /api/plans.
| Plan | Per 30 days | Included | Rate limit | Overage | For |
|---|---|---|---|---|---|
| Free | $0 | 5M RU | 1,000/min | hard stop | A small app. Wallets need no key at all. |
| Builder | $29 | 50M RU | 12,000/min | $5 / M RU | A bot, a small terminal, one contract's indexer. |
| Pro | $199 | 500M RU | 60,000/min | $3 / M RU | Indexers and terminals in production; priority lane for log scans. |
| Dedicated | $999 | 5B RU | 250,000/min | $1 / M RU | A trading terminal's whole backend, and a person to call. |
| Method | RU |
|---|---|
Every eth_*/net_* read not listed below, eth_getBlockByNumber/ByHash without transactions | 1 |
eth_feeHistory | 2 |
eth_estimateGas · eth_createAccessList · eth_getProof · ordo_simulate · eth_getLogs ≤ 100 blocks or by hash | 3 |
ordo_explain | 5 |
eth_getBlockByNumber/ByHash with full transactions | 6 |
eth_getLogs ≤ 1,000 blocks (or an open range) · ordo_quoteSwap | 10 |
eth_sendRawTransaction · ordo_sendRawTransaction · ordo_sendPrivateTransaction · ordo_sendBundle (per transaction) | 12 |
eth_getBlockReceipts | 13 |
eth_getLogs ≤ 10,000 blocks | 25 |
eth_simulateV1 · debug_traceTransaction · debug_traceCall | 30 |
eth_getLogs > 10,000 blocks · debug_traceBlockByNumber/ByHash | 50 |
Keyed execution also carries the protected execution fee: 0.05% of the USD value a confirmed transaction moves (ETH attached plus tokens transferred out), charged to the key once its receipt lands. An approval, a call or a reverted send moves nothing and costs nothing; nothing is added on-chain to what your users sign. Anonymous wallet traffic is metered by IP only and never billed. Over budget on Free returns -32005 naming the plan and the portal; over budget on a paid plan draws overage from prepaid credit until it is spent. Subscription notifications over WebSocket are not metered.
Methods#
- RPCeth_sendRawTransaction
[rawTx]. The sender is recovered from the signature and the call is simulated as that sender witheth_callagainst latest state (the transaction's own gas limit is not forwarded into the simulation). A revert yields-32000ordo: transaction would revert, not submitted: <reason>withdata.ordoProtected = true. Otherwise the raw transaction is sent to the sequencer operator's endpoint and the hash is returned. Keys inauctionmode route through the auction instead and receiveresult.userTxHash.- RPCordo_sendPrivateTransaction
[rawTx]. Always the auction path regardless of key mode. Returns the user transaction hash.- RPCordo_simulate
[rawTx]. Dry run without sending. Returns{ ok, returnData?, revertReason?, from, to }.- RPCordo_sendBundle
[{ txs: rawTx[], allowRevert?: boolean | number[] }]. Up to 10 transactions. See Bundles.- RPCordo_bundlerInfo
[ownerAddress]. Returns{ bundler, executor, deployed, note }: the owner's deterministicOrdoExecutoraddress and whether it exists yet.- RPCeverything else
- Forwarded verbatim. Reads fan out across a failover list of upstreams (rotating on transport failures, HTTP 429/403 and archive-plan refusals); a genuine JSON-RPC error from a healthy upstream is returned with its code intact and does not rotate.
Bundles#
Every transaction in the bundle is simulated first; if any would revert the whole bundle is refused (-32000, data: { failedIndex, hint }) unless that index is allowed by allowRevert. Accepted bundles are dispatched in the same tick.
// response
{
"bundleId": "0x…", // = txHashes[0]
"txHashes": ["0x…", "0x…"],
"atomic": false, // true only when txs.length === 1
"note": "…",
"atomicAlternative": "…" // present when !atomic
}
Ordering on a FCFS chain
Robinhood Chain's sequencer orders by arrival. Firing a bundle in one tick minimises the gap between its transactions but cannot make them adjacent or all-or-nothing. For enforced atomicity, deploy anOrdoExecutor through OrdoBundler and execute the legs as one transaction with on-chain preconditions and a minGainWei guard.Error codes#
| Code | Meaning | Notes |
|---|---|---|
-32700 | parse error | Body is not JSON |
-32601 | <method> is not available without an API key on this endpoint | Anonymous call to a keyed method; standard reads and sends are never affected |
-32001 | unauthorized: the x-api-key presented is not valid | A key header was sent and it does not match any key |
-32005 | rate limit exceeded, retry in Ns | Per key, or per IP for anonymous callers |
-32000 | ordo: transaction would revert, not submitted | data.ordoProtected = true; also used for bundle refusals with data.failedIndex |
-32602 | bundle.txs required · max 10 txs per bundle | Invalid bundle params |
-32000 | all RPC upstreams refused the request | Every upstream failed at the transport level; retry |
| HTTP 404 / 405 | — | GET on unknown path / non-POST RPC; JSON body points at the landing page |
Operational endpoints#
| Endpoint | Returns |
|---|---|
GET/ | Landing page with live probe and one-click wallet setup |
GET/health | { status, upstream, sequencer, uptimeSeconds } |
GET/metrics | Prometheus text: ordo_uptime_seconds, counters (rpc_requests_total, tx_submitted_total, orderflow_auctioned_total, orderflow_fallback_total, send_fallback_total, rpc_unauthorized_total, rpc_rate_limited_total, upstream_challenge_total…), latency p50/p95/p99 |
GET/metrics.json | Same, as { uptimeSeconds, counters, latency } |
Arc, arc.ordofi.networkArc#
Arc is Circle's Layer 1 for stablecoin finance: USDC is the gas token, blocks land every ~500 ms and are final when confirmed. Ordo runs its own Arc node and puts the same gateway in front of it that serves Robinhood Chain. Everything in the gateway reference applies; this section lists what is specific to Arc.
Mainnet launches on 16 September 2026 and arc.ordofi.network will serve it from the first block. Until then the hostname serves Arc Testnet, so anything you build against it today keeps working with only a chain ID change.
Endpoints & chain IDs#
| Mainnet | Testnet (served now) | |
|---|---|---|
| Chain ID | 5042 · 0x13b2 | 5042002 · 0x4cef52 |
| HTTPS RPC | https://arc.ordofi.network | |
| WebSocket | wss://arc.ordofi.network · eth_subscribe for newHeads and logs, plus ordinary calls on the same socket | |
| Explorer | arcscan.app | testnet.arcscan.app |
| Gas token | USDC, 18 decimals as the native currency | |
| Block time | ~500 ms, final on confirmation | |
| Health · metrics | GET /health and GET /metrics on arc.ordofi.network, independent of Robinhood's | |
curl -s https://arc.ordofi.network \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'
# {"jsonrpc":"2.0","id":1,"result":"0x4cef52"} (testnet today; 0x13b2 after mainnet)
Wallets: wallet_addEthereumChain with the parameters above, or the one-click button on arc.ordofi.network. API keys are the same keys you use on Robinhood Chain; usage from both networks draws from one balance and one rate limit per key.
How a send is handled#
eth_sendRawTransaction on Arc follows the same protected path as on Robinhood Chain, minus the auction:
- The transaction is simulated from its recovered sender against current state. If it would revert, it is refused with
-32000and never reaches the network, so no gas is spent on a guaranteed failure. Sends to a precompile, the zero or dead address, or a token approval to an address that is not a contract are refused the same way. - It is delivered directly to the network's RPC, not through a third-party provider or a public relay. Nothing about it is shared before it is included.
- Nonce, balance and receipt reads that the wallet makes around the send are answered from whichever of our node or the network's public endpoints is at the head, checked every second, so a send is never built on a stale nonce.
Tracing is open without a key: debug_traceTransaction, debug_traceCall, debug_traceBlockByNumber and debug_traceBlockByHash run against our own node.
What is live, what is not#
| Live on Arc | Robinhood Chain only, for now |
|---|---|
Standard eth_* reads and eth_sendRawTransaction with simulation and private deliveryWebSocket subscriptions ordo_simulateOpen tracing API keys, plans, rate limits, /health, /metrics |
Order-flow auction and MEV rebates (ordo_sendPrivateTransaction, ordo_sendBundle, ordo_bundlerInfo, POST /submit)Ordo Swap and ordo_quoteSwapLive RPC comparison and the validator watchtower Signed receipts, settlement contracts, sequencer feed relay, MEV measurement |
Calling a Robinhood-only method on arc.ordofi.network returns -32601 method not found. Swap and the auction come to Arc once there is on-chain liquidity worth routing and backrunning; the contracts are chain-agnostic and the gateway already carries the chain profile, so it is a deployment, not a rewrite.
Order-flow auction, auction.ordofi.networkRobinhood Chain#
The auction turns a backrun from something taken to something sold. Lifecycle of one round:
- A raw transaction arrives via
POST /submit(from the gateway for auction-mode keys, or directly from an app). It is parsed, its sender recovered, and it is simulated witheth_simulateV1to derive a hint. - An
opportunityis broadcast to every connected searcher. The window isORDO_AUCTION_WINDOW_MS= 200 ms. - Searchers reply with sealed bids: an amount in wei, their signed backrun transaction, and an EIP-712 Bid signature authorising a maximum charge. Each accepted bid is acknowledged with a signed
BidAck. - At close, the highest bid wins and pays the second-highest (or its own bid if it stood alone). Bids without sufficient bond are rejected at submission.
- The user's transaction is dispatched to the sequencer, then the winner's backrun. Both legs are fault-isolated: a failure of one is reported, never allowed to discard the other.
- The auctioneer calls
OrdoSettlement.settle()with the searcher's signature, debiting the bond and crediting user, app and protocol shares. A signedReceiptfor the round is published and later Merkle-anchored.
POST /submit#
// request
{ "rawTx": "0x02f8…", "originLabel": "my-wallet", "rebateAddress": "0xApp…" }
// response (200)
{
"result": {
"opportunityId": "uuid", "winner": "0x…" | null, "clearingPriceWei": "…",
"bidCount": 2, "userTxHash": "0x…", "backrunTxHash": "0x…" | null, "dispatchedAt": 1756…
},
"rebate": { … } | null, // ledger entry: user / app / protocol amounts
"settlement": { … } | null, // what was submitted to OrdoSettlement
"settlementTxHash": "0x…" | null,
"receipt": Receipt | null, // signed, see Verifiability
"settlementContract": "0xbC68…",
"userError": null | "…", // the user leg's dispatch error, if any
"auctionDelayMs": 203, // how long the hold added
"hint": { "level": "pools", "simulated": true, "pools": 1 }
}
A malformed request returns 400 { error }. auctionDelayMs is reported rather than hidden: it is the latency cost of the auction, typically the window plus a few milliseconds.
Searcher WebSocket, wss://auction.ordofi.network/searcher#
All frames are JSON with a type. Server → client:
// on connect
{ "type": "welcome", "swapTopics": { "0xd78a…": "univ2", "0xc420…": "univ3", "0x40e9…": "univ4" },
"auctionWindowMs": 200, "hintLevel": "pools" }
// one per held transaction
{ "type": "opportunity", "opportunity": {
"id": "uuid", "createdAt": 1756…,
"hint": { "poolsTouched": ["0x…"], "swaps": [ { "kind": "univ3", "pool": "0x…", "direction": "0for1" } ],
"to": "0x…", "selector": "0x…", "value": "0x0", "simulated": true, "level": "pools" },
"originLabel": "my-wallet", "originRebateAddress": "0x…" } }
// reply to a bid
{ "type": "bid_ack", "opportunityId": "uuid", "accepted": true,
"ack": { "opportunityId": "0x…32 bytes", "searcher": "0x…", "bidWei": "…", "receivedAt": 1756…, "signature": "0x…" } }
{ "type": "bid_ack", "accepted": false, "reason": "insufficient bond: X wei bonded, Y wei bid, deposit into OrdoSettlement first" }
// malformed frame
{ "type": "error", "error": "invalid json" }
Client → server:
{ "type": "bid", "opportunityId": "uuid", "searcher": "0xYou",
"bidWei": "1000000000000000", // decimal string
"backrunRawTx": "0x02f8…", // signed, ready to dispatch
"bidSig": "0x…" } // EIP-712 Bid over (searcher, opportunityId, maxAmountWei = bidWei)
Rejection reasons: unknown or closed auction, auction closed, invalid bidWei, bid must be positive, missing backrunRawTx, searcher must be an address when bonding is enabled, insufficient bond…. There is no per-round result frame over the socket; the outcome is the signed receipt at GET /receipts/:opportunityId, and the winner learns it dispatched by watching the chain.
Hints#
Searchers never see the raw transaction. The hint is derived by simulating it with eth_simulateV1 and decoding Swap logs against the venue topic map. ORDO_HINT_LEVEL selects how much is shared:
| Level | Fields |
|---|---|
minimal | poolsTouched, to, selector, value |
pools (default) | + swaps[] with kind (univ2/univ3/univ4), pool, poolId for V4, direction (0for1/1for0) |
full | + amount0, amount1 per swap |
If simulation fails or the upstream degrades, simulated is false and poolsTouched falls back to [to]. This is exactly the information that becomes public on the sequencer feed the moment the transaction is ordered; the auction reveals it a few hundred milliseconds early to whoever pays for the privilege.
Clearing rule & bonds#
- Sealed bids; the highest
bidWeiwins. - Clearing price = second-highest bid; a lone bidder pays its own bid; no bids → no winner, price 0.
- When
ORDO_SETTLEMENT_ADDRESSis set, a bid is only accepted ifOrdoSettlement.bond(searcher) ≥ bidWei(bond reads cachedORDO_BOND_CACHE_MS= 15 s). - On-chain, the charge can never exceed the searcher's signed
maxAmountWei; the auctioneer proves the second price by charging less than or equal to what the searcher authorised. - Opportunity ids are UUIDs; on-chain they become
bytes32by stripping dashes and right-padding with zeros.
Rebate split#
| Recipient | Share | Where it lands | Env |
|---|---|---|---|
| User (transaction sender) | 90% | claimable[user] in OrdoSettlement | ORDO_REBATE_USER=0.9 |
| App (order-flow originator) | 5% | claimable[rebateAddress] | ORDO_REBATE_APP=0.05 |
| Protocol | 5% | claimable[treasury] | ORDO_REBATE_PROTOCOL=0.05 |
The off-chain ledger (data/rebates.ndjson) mirrors every entry and tracks app balances owed; the on-chain split is fixed by appBps/protocolBps in the contract and claimed with claim().
HTTP endpoints#
| Endpoint | Returns |
|---|---|
POST/submit | Auction result, see above |
GET/health | { status, searchers, stats } · stats = { opportunities, bids, rejectedBids, dispatched, backruns, settled } |
GET/stats | { stats, connectedSearchers, rebateSplit, owedRebates } |
GET/receipts?n=20 | { auctioneer, receipts[] } |
GET/receipts/:opportunityId | { auctioneer, receipt } by uuid or bytes32 · 404 if unknown |
GET/receipts/root | { root, count, anchoredCount, auctioneer } |
GET/feed/stats | { upstream, clients, sequencerMessages, txsRelayed, lastSequenceNumber, reconnects } |
WSS/searcher · /feed | See searcher protocol and feed relay |
OrdoSettlementRobinhood Chain#
Trust-minimised payment for auction outcomes. Searchers post ETH bonds; the auctioneer can debit a bond only with the searcher's own EIP-712 signature over a maximum for that specific opportunity, at most once per opportunity.
| Function | Access | Semantics |
|---|---|---|
payabledeposit() · receive() | anyone | Adds msg.value to bond[msg.sender]. A plain ETH transfer to the contract bonds the sender. |
requestWithdraw(uint256 amount) · cancelWithdraw() · withdrawBond() | bond owner | Leaving is two steps. requestWithdraw starts a clock (WITHDRAW_DELAY, 120 s); the amount stops counting as collateral for new bids but still backs wins already made. After the delay, withdrawBond() pays out the request or whatever settlements left behind, whichever is less. This is what makes a win always collectable: a searcher cannot pull the bond between the back-run landing and settle. |
collateral(address) | view | bond less any pending withdrawal, what the auctioneer accepts bids against |
settle(Settlement s, bytes searcherSig) | auctioneer only | Verifies searcherSig over Bid(searcher, opportunityId, maxAmountWei); requires chargeWei ≤ maxAmountWei, !settled[opportunityId], EIP-2 low-s. Debits bond[searcher] by chargeWei and credits claimable for user (remainder), app (appBps) and treasury (protocolBps). Marks the opportunity settled. |
claim() | anyone | Pays out claimable[msg.sender] |
bond(address) · claimable(address) · settled(bytes32) | view | Balances and replay state |
bidDigest(address, bytes32, uint256) | view | The exact EIP-712 digest a searcher signs; useful for wallets and audits |
DOMAIN_SEPARATOR() · BID_TYPEHASH() · auctioneer() · protocolTreasury() · appBps() · protocolBps() · owner() | view | Parameters |
setAuctioneer · setSplit · setProtocolTreasury · transferOwnership | owner only | Administration. The owner is the deployer; see trust model. |
struct Settlement {
address searcher; // whose bond is debited
bytes32 opportunityId; // uuid → bytes32 (dashes stripped, right-padded)
uint256 maxAmountWei; // what the searcher signed
uint256 chargeWei; // what the auction cleared at (≤ maxAmountWei)
address user; // receives the user share
address app; // receives appBps
}
EIP-712 bid#
domain = { name: "OrdoSettlement", version: "1", chainId: 4663, verifyingContract: 0xbC680922DaF2F65a8B957e5238857f8c68BeDabb }
Bid = Bid(address searcher, bytes32 opportunityId, uint256 maxAmountWei)
digest = keccak256(0x1901 ‖ DOMAIN_SEPARATOR ‖ keccak256(abi.encode(BID_TYPEHASH, searcher, opportunityId, maxAmountWei)))
The SDK signs this with signTypedData; maxAmountWei equals the bid, so a searcher can never be charged more than it bid, and the contract makes the second-price claim falsifiable: any chargeWei > maxAmountWei reverts.
OrdoBundler & OrdoExecutorRobinhood Chain#
Enforced atomicity on a chain whose sequencer offers none. OrdoBundler deploys one OrdoExecutor per owner at a deterministic CREATE2 address; the executor runs a list of calls as a single transaction, with preconditions and a minimum-gain guard, so a stale or unprofitable bundle reverts as a whole.
| Function | Semantics |
|---|---|
OrdoBundler.executorOf(address owner) → address | Deterministic executor address, whether or not deployed |
OrdoBundler.isDeployed(address owner) → bool · deploy(address owner) · deploy() | Deploys the owner's executor; emits ExecutorDeployed |
OrdoExecutor.execute(Call[] calls, Check[] checks, uint64 maxBlock, uint256 minGainWei) payable → bytes[] | Owner only. Reverts if block.number > maxBlock, if any Check fails before execution, or if the executor's ETH balance did not grow by at least minGainWei. |
OrdoExecutor.checksPass(Check[]) → bool | Evaluate preconditions off-chain via eth_call |
withdraw · withdrawToken · receive() | Owner recovers ETH and ERC-20 balances |
Use ordo_bundlerInfo on the gateway to find your executor address; then a "bundle" is one execute() transaction sent like any other.
OrdoReceiptLogRobinhood Chain#
| Function | Semantics |
|---|---|
commit(bytes32 root, uint64 count) | Auctioneer only (settlement.auctioneer()). Appends a Merkle root over the first count receipts. Rejects an empty root or a count smaller than the previous commitment. |
latest() → (root, count) · total() · commitments(uint256 i) | Read the anchor history |
verify(bytes32 leaf, bytes32[] proof, uint256 index) → bool | Inclusion proof against the latest root |
Signed receiptsRobinhood Chain#
A sealed-bid auction asks you to trust the operator about the bids you did not see. OrdoFi removes that: the moment a bid lands it is acknowledged under the auctioneer's EIP-712 signature, and when the round closes a receipt listing every bid is signed too. A discrepancy between an ack and a receipt is cryptographic evidence of misconduct, not a complaint.
domain = { name: "OrdoAuction", version: "1", chainId: 4663 } // no verifyingContract; these are off-chain attestations
BidAck = BidAck(bytes32 opportunityId, address searcher, uint256 bidWei, uint64 receivedAt)
Receipt = Receipt(bytes32 opportunityId, bytes32 bidsHash, address winner, uint256 clearingPriceWei, uint64 closedAt)
bidsHash = keccak256(abi.encode(address[] searchers, uint256[] bidsWei, uint64[] receivedAts)) // keccak256("") when empty
receiptHash = keccak256(abi.encode(opportunityId, bidsHash, winner, clearingPriceWei, closedAt)) // the Merkle leaf
// GET /receipts/:opportunityId
{ "auctioneer": "0x8942…C649",
"receipt": {
"opportunityId": "0x…", "winner": "0x…", "clearingPriceWei": "…",
"bids": [ { "searcher": "0x…", "bidWei": "…", "receivedAt": 1756…, "bidSig": "0x…" } ],
"closedAt": 1756…, "signature": "0x…" } }
Each listed bid carries the searcher's own settlement signature (bidSig), so the auctioneer cannot invent bids to inflate a clearing price: a fabricated bid would need a signature it cannot forge.
Merkle anchoring#
Signatures make a receipt unforgeable but not un-retractable. To stop a receipt from being quietly replaced after you read it, the auctioneer commits a Merkle root over every receipt it has ever issued to OrdoReceiptLog every ORDO_RECEIPT_COMMIT_EVERY = 25 new receipts. Leaves are receiptHash in issue order; parents are keccak256(sorted(left, right)); an odd node is carried up unpaired. GET /receipts/root reports count (issued) and anchoredCount (immutable): the gap is the only part still retractable, and it is bounded.
Receipts persist in data/receipts.ndjson and are rehydrated on restart; the anchor state is re-read from the chain at boot so a restart cannot reset the log.
Auditing a round#
| Claim | Check |
|---|---|
| My bid was counted | The acknowledged (searcher, bidWei, receivedAt) appears in the receipt's bids |
| No bids were invented | Every bidSig recovers to its searcher under the OrdoSettlement domain |
| Second price was honoured | winner is the highest bidder; clearingPriceWei equals the second-highest bid (or the sole bid) |
| The receipt is the auctioneer's | signature recovers to the published auctioneer under the OrdoAuction domain |
| It cannot be swapped later | receiptHash is included under an anchored root (verify()) |
import { auditReceipt } from "@ordofi/core/receipt";
const { receipt, auctioneer } = await fetch(`https://auction.ordofi.network/receipts/${id}`).then(r => r.json());
const finding = await auditReceipt(receipt, myAck, 4663, auctioneer, "0xbC680922DaF2F65a8B957e5238857f8c68BeDabb");
if (!finding.ok) console.error(finding.reason); // signed proof of what went wrong
// also: auditClearingPrice(receipt), auditBidAuthenticity(receipt, 4663, settlement)
Sequencer feed relay, wss://auction.ordofi.network/feedRobinhood Chain#
MEV here starts on the sequencer feed. OrdoFi holds one upstream connection to wss://feed.mainnet.chain.robinhood.com, decodes the Nitro framing, and fans the transactions out so a searcher does not need its own decoder or connection.
// on connect
{ "type": "welcome", "feed": "wss://feed.mainnet.chain.robinhood.com", "note": "…" }
// per sequencer message
{ "type": "feed_txs", "sequenceNumber": 51709518, "blockNumber": 51654343, "timestamp": 1756…,
"receivedAt": 1756…, // ms, stamped by the relay on receipt
"txs": [ { "hash": "0x…", "raw": "0x02f8…" } ] }
Decoding: L1 message kind 3 carries an L2 message; L2 kind 4 is a single signed transaction, kind 3 is a batch of sub-messages each prefixed by a big-endian uint64 length. receivedAt lets you measure the relay's own added latency instead of assuming it. The upstream reconnects with backoff 1 s → 30 s; GET /feed/stats exposes reconnects.
What the feed is not
The feed carries transactions the sequencer has already ordered. It is a head start on decoding, not a preview of pending flow. Pending flow, with hints and the right to trade after it, is what the auction sells.Swap API for appsRobinhood Chain#
For apps that execute swaps on behalf of their users, in particular ERC-4337 smart accounts (embedded wallets with sponsored gas). One call returns the best route across Uniswap V3, V4, hooked pools and the aggregators we compare against, a floor that reflects that route, your own fee paid in the same transaction, and the calls ready to execute. Optionally, the whole batch simulated from the user's account first, so you know a swap lands before you sponsor its gas.
POST https://rpc.ordofi.network/swap/v1/build
x-api-key: <your key>
content-type: application/json
{
"tokenIn": "0x0000000000000000000000000000000000000000", // ether; or an ERC-20 address
"tokenOut": "0xfe2f0fb0c00d19786a8abf98d4b1f1ac8763b167",
"amountIn": "1000000000000000000", // smallest unit, decimal or hex string
"from": "0x<the user's smart account or EOA>",
"recipient": null, // defaults to from
"slippageBps": 50, // 1..1000; default 50
"appFee": { "bps": 50, "recipient": "0x<your fee address>" }, // optional, max 300
"simulate": true, // optional
"fast": false // true: route only, no back-run search; for quotes while typing
}
| Field in the answer | Meaning |
|---|---|
calls[] | For a smart account: execute in order, atomically. Each has to, data, value and a kind: appFee (your fee, from the input, in the input token), approve (exactly the swap amount, to the swap contract), swap. |
tx, approval | For an EOA: the swap alone, and the approval to send first if any. |
amountOut, minOut | The quoted output and the floor the calldata enforces: amountOut × (1 − slippageBps). A fill below the floor reverts and the user keeps their input. |
amountIn, swapAmountIn, appFee | What the user sends, what reaches the swap after your fee, and the fee itself: bps, recipient, amount, token. |
provider, quotes[] | Who executes (ordo, or an aggregator that quoted clearly more) and everyone who priced the order, best first. Pass "ordoOnly": true to never execute through a third party. |
priceImpactBps, route, splits, reclaim | How far this size moves the market; the route in words; the split when the order is spread across pools; the surplus the contract reclaims for the user in the same transaction, when there is one. |
simulation | With simulate: true: ok, or failedAt (index into calls) and reason in words, plus the output the simulated swap actually returned. An account that does not hold the input fails at the fee or approval step, before any gas is spent. |
expiresAt | Quotes move; ask again after this. |
| Latency | About 1 s with fast: true (route only), 2–3 s for a full answer with the back-run search and aggregator comparison, plus ~0.8 s for simulate. Quote with fast while the user types; build without it when they press buy. |
Errors are 400 for a bad request (with the field named), 429 over your key's limit, 502 when no venue could price the order. A key is not required for testing (anonymous callers share the per-IP limit), but an app should use one. Requests count against your key like any other; a partner plan comes with a dedicated gateway pool that no other traffic shares. Ordo takes no fee on swaps; the only cost inside the route is the pool's LP fee, and the aggregator's own if one executes.
What this replaces: an aggregator quote signed with a 45% tolerance. Here the floor is 0.5% by default and can be no wider than 10%; if a trade cannot fill within it, the answer is a revert you saw coming in simulation, not a fill 40% below the quote.
Ordo Execution, exec.ordofi.networkRobinhood Chain#
Tell Ordo the result you want, not the transaction to send. The user signs one order: sell exactly this much of one token for at least that much of another, before a deadline. Ordo finds the route, simulates the whole execution, pays the gas, sends it, follows it to finality and recovers from what goes wrong on the way. The user gets at least their minimum, once, or nothing moves.
The order is a Permit2 signature with the order attached as its witness, spent by OrdoExecutor. Permit2 spends a signature only when the transaction that uses it succeeds, so Ordo can retry as often as it needs to and the order still runs at most once. The user needs no ETH. The input must be an ERC-20 the user has approved for Permit2 once (most Uniswap users already have); the quote says when that approval is missing and returns it ready to send. Ordo's fee is 0.1% of what the user receives, part of the signed order, and the contract refuses anything above 0.3%.
Quote, sign, submit#
// 1. Quote: a route priced now, and the order to sign for it
POST https://exec.ordofi.network/v1/quote
{ "owner": "0x<user>", "tokenIn": "0x<USDG>", "tokenOut": "0x<TSLA>",
"amountIn": "1000000000", // smallest unit
"slippageBps": 50, // default 50, at most 1000
"deadlineSec": 300, // default 300, at most 3600
"recipient": null } // defaults to owner
→ { "id": "0x…", // the execution id, known before signing
"intent": { … }, // the order, every number a decimal string
"typedData": { … }, // pass to eth_signTypedData_v4 as is
"quote": { "amountOut", "fee", "feeBps": 10, "expectedOut", "minOut", "route", "gasPaidByOrdo": true },
"approval": null | { "to", "data", "note" } } // the one-time Permit2 approval, when missing
// 2. The user signs; no transaction, no gas
const signature = await wallet.request({ method: "eth_signTypedData_v4", params: [user, JSON.stringify(q.typedData)] });
// 3. Submit; the same order submitted twice returns the same execution
POST https://exec.ordofi.network/v1/submit
{ "intent": q.intent, "signature": "0x…" }
→ { "id", "status", "waiting", "statusUrl" }
// 4. Follow it
GET https://exec.ordofi.network/v1/status/<id>
| Field in the status | Meaning |
|---|---|
status | queued (working on it), submitted (a transaction is out), confirmed, or a final expired / cancelled / failed. The last three mean nothing moved. |
waiting | What a queued order is waiting for, in words: the price to come back to the minimum, the owner's balance or approval. |
result | tx, block, amountOut (received after the fee), fee, refund (input the route did not use), surplus (ether a route reclaimed for the user), gasCostWei (paid by Ordo), and finality: soft (sequenced), posted (its batch is on Ethereum, with batch), final (Ethereum finalized it, with l1Confirmations). |
steps, recoveries | Every action taken, with times and transaction hashes, and how many of them were recoveries. |
txs | Every transaction signed for this order. Only one of them can have run it. |
GET /v1/stats | Orders, success rate, median and p95 time from signature to confirmation, recoveries, gas paid. |
Errors are 400 with the reason (a bad field, a signature that is not the owner's, an order too small for its 0.1% to cover the gas Ordo pays) and 503 with retry: true when the network is busy. A user cancels an order by spending its nonce on Permit2 (invalidateUnorderedNonces) or by letting the deadline pass.
What it recovers from#
| What happens | What Ordo does |
|---|---|
| The price is under the user's minimum | Waits and re-routes every couple of seconds until the deadline, instead of failing the first try. Over the minimum, the extra goes to the user, never to Ordo. |
| The simulation fails, or an attempt reverts on chain | Nothing moved: the signature is spent only on success. Routes again, simulates again, resends. |
| A transaction is not picked up | Sends the same signed bytes to every endpoint again (Ordo's gateway and the sequencer directly), then replaces it under the same relayer nonce with a fresh route and a higher fee. Whichever lands, the order runs once. |
| A block is replaced | Until the order is final, its block is re-checked. If the chain no longer has the transaction, Ordo asks the chain whether the order still needs to run, and runs it. |
| The money arrives after the signature | A balance or Permit2 approval that is short keeps the order waiting, not failed, until the deadline. |
| An endpoint fails | Reads go through Ordo's gateway, which fails over between providers; sends go to several endpoints at once. |
What the relayer can and cannot do: it chooses the route and pays the gas. It cannot change anything the user signed (amount, minimum, recipient, fee, deadline), cannot call any contract but the allowlisted swap contracts, and the swap contract's allowance is exactly the order's input, reset to zero in the same transaction. The worst a faulty relayer can do is fill an order at exactly its minimum. Only Ordo's relayer executes, so nobody else can race it to an order with a route that pays the minimum and keeps the rest. Every order is priced through Ordo Swap and KyberSwap side by side, and the route that pays the user more runs; both contracts are on the executor's allowlist. Robinhood Chain only, for now.
Price feeds, feeds.ordofi.networkRobinhood Chain#
Dollar prices for Robinhood Chain's stock tokens, worked out on chain from the Uniswap V3 pools each stock trades in and averaged over 30 minutes. Every stock has its own feed contract that answers exactly like a Chainlink AggregatorV3Interface, so a lending market, perp or vault written for Chainlink reads it without changes. Free to read: no key, no sign-up, no fee.
Every feed address, its live price and the pools it reads: feeds.ordofi.network. The same list as JSON, CORS-open: GET https://feeds.ordofi.network/api/feeds. On chain, the registry at 0x7DD461f4a56422E419C7F2d0cF44d58600F860d8 lists them all.
Reading a feed#
// TSLA / USD on Robinhood Chain (chain 4663) AggregatorV3Interface feed = AggregatorV3Interface(0x2a1f40d04795832e9e1a00949cbf58ff22be6dde); (, int256 price,, uint256 updatedAt,) = feed.latestRoundData(); // price has 8 decimals: 37256317700 = $372.563177 // Or everything at once, from the same contract: (uint256 mean, uint256 spot, uint256 gapBps, bool marketOpen) = OrdoStockFeed(feed).prices(); // Find a feed from the stock token: address feed = OrdoPriceFeeds(0x7DD461f4a56422E419C7F2d0cF44d58600F860d8).feedOf(stockToken);
| Feed (one per stock) | Returns |
|---|---|
latestRoundData() | The 30-minute average in dollars, 8 decimals. roundId and answeredInRound are the block timestamp; startedAt and updatedAt are the block timestamp too, because the average is worked out fresh on every call. |
latestAnswer() · latestTimestamp() · latestRound() | The older Chainlink interface, same values. |
decimals() · description() · version() | 8, e.g. "TSLA / USD", 1. |
getRoundData(roundId) | Only the current round (roundId equal to the block timestamp). Past rounds were never stored and revert with NoHistory(). |
prices() | mean (the 30-minute average), spot (the price now), deviationBps (the gap between them) and marketOpen. |
stock() · registry() | The stock token it prices, and the registry it reads from. |
| Registry | Returns |
|---|---|
feedOf(stock) | The feed for a stock token, or the zero address if it is not listed. |
count() · stockAt(i) | Every listed stock, by index. |
priceOf(stock) · deviationBps(stock) | The same average and price now as the feed, by stock address. |
routeOf(stock) · pendingRoute(stock) | The pools the price is read from, and any change waiting with the time it can apply (eta, 0 when none). |
marketOpenNow() · marketOpen(ts) | Whether the New York Stock Exchange is in its regular session now, or at any unix time: 9:30 to 16:00 Eastern on weekdays, daylight saving time worked out on chain, holidays and 13:00 early closes from the exchange calendar. |
How the price is made. Each stock is priced along up to three pools ending in USDG, counted as $1. The answer is the time-weighted average over the last 30 minutes (WINDOW = 1800), read from the pools' own price history. Moving a pool for one block barely moves a half-hour average; holding it off its price for that long means paying every arbitrageur the whole time. A pool without 30 minutes of history cannot be listed, and if its history runs short the feed reverts rather than answer from too little. Only stocks where a $1,000 trade moves the price by less than 2% are listed.
Using it safely#
| Topic | What to do |
|---|---|
| Freshness | updatedAt is always the current block, so a Chainlink-style staleness check always passes and does not protect you here. The real failure mode is a revert, when a pool's history runs short. Call the feed inside try/catch and treat a revert as "no price": pause new borrows, do not liquidate. |
| The gap | prices().deviationBps is how far the price now sits from the average. A wide gap means the market is moving fast or someone is pushing a pool. Refusing to open positions or liquidate while it is above a few percent is cheap insurance. |
| Market hours | Stock tokens trade around the clock; the real market does not. A price on Saturday reflects weekend trading on chain and can jump when New York opens on Monday. marketOpen lets you use tighter limits, or pause new positions, while the exchange is closed. |
| Depth | A thinly traded stock is cheaper to push than a busy one. The site lists the pools behind every feed; size your caps to their depth, not to the price alone. |
| Changes | A new stock is listed at once. Changing or removing an existing stock's pools waits 48 hours on chain (ROUTE_DELAY), emitted as RouteProposed and shown on the site, and anyone can apply it once the wait is over. Watch that event for the stocks you use. |
| Who controls what | The registry's owner (Ordo) can list new stocks, propose pool changes behind the 48-hour wait, and set the exchange calendar for holidays. It cannot set or override a price. Source verified on Sourcify. |
Bridge APIAll networks#
Between the two chains Ordo runs nodes on: ETH or USDG on Robinhood Chain into USDC on Arc, and back. We are not the bridge and the answer says so: the transfer is filled by whichever of the bridges serving both chains quoted best, and your funds are in their hands between the two chains. What Ordo adds is the comparison, a deposit simulated from the sender's own account before a wallet is opened, and delivery through the protected send path.
POST https://rpc.ordofi.network/bridge/v1/quote
content-type: application/json
{
"fromChain": 4663, "toChain": 5042,
"fromToken": "0x0000000000000000000000000000000000000000", // the zero address is the chain's gas token
"toToken": "0x0000000000000000000000000000000000000000", // on Arc that is USDC, 18 decimals
"amount": "100000000000000000",
"from": "0x<sender>",
"slippageBps": 100,
"simulate": true
}
| Field in the answer | Meaning |
|---|---|
best, quotes[] | The route that executes, and everyone who priced it. Each carries from/to (chain, token, symbol, decimals, amount), minOut as the provider enforces it, impactBps, feeUsd and seconds. |
best.tx | The one transaction to sign on the source chain: to, data, value, chainId, gas. Native on both sides, so there is no approval step. |
best.track | { provider, id } for GET /bridge/v1/status?provider=&id=, which answers pending, done, failed or unknown with the destination transaction once there is one. LI.FI tracks by the source hash, so pass that as id. |
simulation | With simulate: true and a gateway on the source chain: whether the deposit would succeed, and why not. A transfer that cannot work is refused here rather than after it is paid for. |
filledBy | Who holds the money in flight, in words, for you to show the user. |
Arc's USDC is the gas token with 18 decimals; the six-decimal ERC-20 USDC on Arc is a different asset. decimals travels with every side of every quote so the two cannot be confused. Interface: bridge.ordofi.network.
Without a wallet#
For a user who cannot sign, or is moving funds out of an exchange: ask for a deposit address, and they send to it like any other transfer. The address belongs to the filler, is computed for that one order, and pays out to the address the user named. Ordo never holds the funds, and neither does the page.
POST https://rpc.ordofi.network/bridge/v1/deposit
{ "fromChain": 4663, "toChain": 5042,
"fromToken": "0x0000000000000000000000000000000000000000",
"toToken": "0x0000000000000000000000000000000000000000",
"amount": "100000000000000000",
"recipient": "0x<where the funds should land>" }
→ { "depositAddress": "0x…", "chainId": 4663,
"send": { "symbol": "ETH", "decimals": 18, "amount": "100000000000000000" },
"receive": { "symbol": "USDC", "decimals": 18, "amount": "…", "minAmount": "…" },
"recipient": "0x…", "flexibleAmount": true, "seconds": 1, "feeUsd": 1.34 }
| Then | What happens |
|---|---|
| The user sends | A plain transfer of the named asset on the named chain. No calldata, no approval. A route that would need calldata is refused rather than offered, because someone sending from an exchange can only make a plain transfer. |
| A different amount | Re-priced at the rate when it arrives and filled, as long as it still covers fees. flexibleAmount says so. |
| It cannot be filled | Refunded automatically to the address it came from. We always set the filler's refund flag; without it there is no automatic refund at all. This is why the interface tells users to send from a wallet they control rather than from an exchange account. |
GET /bridge/v1/deposit-status?address= | waiting, pending, done or failed, with the deposit and destination transactions. Poll by deposit address, not request id: a re-priced deposit gets a new id and the old one would say "waiting" while the money landed. |
Apps & walletsAll networks#
Routing your users' transactions through OrdoFi costs one header and returns a share of the backrun value that would otherwise leave through a bot.
- POSTapp.ordofi.network/api/keys
- Body
{ label?, rebateAddress? }. Returns201 { key, label, mode, rebateAddress, rateLimitPerMin, note, rpc }. With a rebate address the key isauctionmode; without,direct(protected passthrough only). Limited to 3 keys per hour per IP. Keys look likeordo_+ 36 hex characters and are shown exactly once. - GETapp.ordofi.network/api/account?address=0x…
{ address, settlement, bondEth, claimableEth, settlementsAsSearcher, settlementsAsApp, claimHint }. Bond and claimable are read live from the contract; no key required.- on-chainOrdoSettlement.claim()
- Withdraws everything credited to
msg.sender: user rebates, app shares, or protocol shares. The portal at /portal wraps this.
Use the key as x-api-key against https://rpc.ordofi.network, or call POST auction.ordofi.network/submit directly with rebateAddress in the body if you prefer to see the auction result inline.
Searcher SDK, @ordofi/sdkRobinhood Chain#
TypeScript, built on viem and ws. Until the npm release lands, install from the monorepo (packages/sdk).
new OrdoSearcher(config)config = { auctionWsUrl, privateKey, settlementAddress, onOpportunity, chainId? }.connect()opens the socket and reconnects on close with exponential backoff (1 s doubling to a 30 s cap, reset on a successful open);close()stops it. Exposesaddress.onOpportunity(opp, { account }) → Promise<{ maxBidWei: bigint, backrunRawTx: Hex } | null>- Called per opportunity with
opp = { id, hint: { poolsTouched, to, selector, value }, originLabel }. Returnnullto pass. A returned decision is signed (signBid) and sent as abidframe withbidWei = maxBidWei. searcher.signBid(opportunityId, maxAmountWei) → Promise<Hex>- EIP-712
Bidsignature under the OrdoSettlement domain; also usable to build bids by hand. opportunityIdToBytes32(uuid) → Hex- Strip dashes, right-pad to 32 bytes.
new OrdoBond(privateKey, settlementAddress, rpcUrl?)deposit(amountEth),claim(),bondOf(address),claimableOf(address).submitOrderFlow(url, { rawTx, originLabel, rebateAddress? })- Typed wrapper for
POST /submit. SETTLEMENT_ABI- The subset of the settlement ABI the SDK uses.
Bidding economics
Pay at most what the backrun is worth to you net of gas; you will be charged the second price, so shading is unnecessary. Bond at least your largest intended bid, since bids above your bond are rejected before the auction. The backrun transaction must be valid at dispatch time: correct nonce, sufficient gas for a state-changing call (a 21,000-gas placeholder is refused by the chain), and a fee at or above the current base fee.Web API, app.ordofi.network/apiAll networks#
Read-only JSON over HTTPS, CORS-open, default cache-control: public, max-age=15 unless noted. Large responses are gzipped when accepted.
| Endpoint | Returns |
|---|---|
GET/api/stats | Embed-friendly headline numbers: { chain, arbs, searchers, pools, swaps, arbsPerDay, settlement: { deployed, address, settlements, totalSettledEth, rebatesToUsersEth }, updatedAt }. arbsPerDay is null until the sample spans one hour. |
GET/api/report | The watcher's full MEV report: venues, pools, searchers, USD totals |
GET/api/arbs/recent?n=40 | Latest attributed backruns (max 200): block, tx, sender, pools, profit token and amount |
GET/api/onchain?address= | Settlement contract statistics from the index, with an on-chain log scan as fallback |
GET/api/auction | Proxy of the auction's /stats |
GET/api/explorer | { report, recentArbs, auction, onchain, generatedAt }, everything the explorer page renders |
POST/api/keys · GET/api/account | See Apps & wallets |
Trade endpoints#
The terminal at /trade is a thin client over these. The server never holds keys or sends transactions; it prepares Uniswap V3 calldata that the user's wallet signs and submits through the protected RPC. Prices come from the pools' own Swap events, never from an oracle or a price API.
| Endpoint | Params | Returns |
|---|---|---|
/api/trade/chain | — | { id: 4663, idHex: "0x1237", name, rpc, explorer } for wallet_addEthereumChain |
/api/trade/tokens | — | Tradable assets: { address, symbol, name, decimals, usdPerToken, icon, holders, active, tradable, swaps24h, tiers: { eth: [fees], usdg: [fees] } }[]. Sources: the explorer's most-held ERC-20s, every factory pool that traded in the last 24 h, and user imports. tradable is true when a Uniswap V3 pool exists against ETH or USDG, null while the check is queued. Stale-while-revalidate, rebuilt every 5 min. |
/api/trade/markets | — | Every routable pair that traded today, from the recorder's tape alone (no RPC on the request path): { markets: [{ pool, fee, base, quote, price, change24, change1h, high24, low24, volumeQuote, volumeUsd, swaps, lastTrade }], pairsTotal, coverage, at }. Money (USDG over ETH) is always the quote; top 800 by volume. |
/api/trade/token | address | Import any ERC-20 by address: resolves metadata and checks ETH/USDG routability. Tradable imports join the list. |
/api/trade/quote | tokenIn, tokenOut, amountIn (raw units; eth for native), slippageBps?=50, from? | Best of direct and one-intermediate (WETH/USDG) V3 routes via QuoterV2: { amountIn, amountOut, minOut, route: { tokens, fees, hops }, priceImpactBps, gasEstimate, tokenIn, tokenOut, for, guard, tx: { to, data, value } | null, approval?, account?, quotedAt }. tx is a SwapRouter02 multicall with exactInput paying MSG_SENDER; for native output the swap pays the router and a trailing unwrapWETH9(amountMinimum) pays the sender in ETH. 10-minute deadline. Proof of delivery: tx is only present when from is given and the call has been executed with eth_simulateV1 from that address, with from receiving at least minOut of the output, paying at most amountIn, and nothing landing on 0x00…0a/0xdead or staying in the router. guard reports the result: { ok, reason, received[], paid[], leaks[], retained[], via }. Never send calldata from a quote without guard.ok, and only from the for address. no-store. |
/api/trade/candles | base, quote, bucketSec?=60, spanBlocks?=72000 | OHLCV of base in quote from the deepest direct pool; the watcher's recorded tape first, an adaptive eth_getLogs walk to backfill. { pool, fee, base, quote, source: "recorder" | "logs" | "recorder+logs", truncated, swaps, volumeQuote, last, candles[] } |
/api/trade/pair | base, quote | { pool, fee, tvlUsd, reserves, base, quote } for the deepest direct pool by in-range liquidity; cached 60 s |
/api/trade/trades | base, quote, limit?=40 | Most recent individual swaps: { block, time, tx, logIndex, price, sizeBase, sizeQuote, side }; cached 5 s |
/api/trade/balances | address | Native ETH and every listed token with a non-zero balance, USD-valued where a price is known; cached 30 s |
/api/trade/resolvers | — | Health of the persistent lookup caches: { pools, tokens, routes } each { known, pending } |
MEV measurementRobinhood Chain#
The watcher follows the head with concurrent block fetches (0.1 s blocks demand it) and classifies every successful transaction whose receipts contain Swap logs from the venue topic map (univ2, univ3, univ4, which also covers forks sharing those event signatures).
- Candidate. Two or more distinct pools touched in one transaction.
- Net flows. ERC-20
Transfers are netted for the transaction'sfromandtoonly (not arbitrary recipients), and nativevaluecounts as WETH out. This is what stopped a user's full swap output from being booked as a third party's profit. - Disqualifier. If any token nets negative, the sender paid something: it is a trade, not an arbitrage.
- Profit. Positive net in a quote asset (USDG, WETH) is booked as
profitWeiin that token; a pure positive position in another token is booked as a weaker, unpriced arbitrage. - Valuation. Per token, with decimals and price from
getTokenInfo; ETH/USD is derived from the on-chain WETH/USDG pool, not a constant. - Extrapolation. Daily rates are not published until the sample spans at least one hour.
Alongside attribution the watcher records a one-minute OHLC tape per pool: Uniswap V3 pools by address, V4 pools by PoolId, with their keys from the PoolManager's Initialize events in v4_pools, from sqrtPriceX96 and swap amounts (candles table, pruned after three days). The index lives in SQLite (arbs, arb_pools, auctions, settlements, candles, api_keys, meta) with NDJSON as the raw record.
Self-hosting#
git clone https://github.com/OrdoFi/ordofi && cd ordofi && npm install npm run watcher # index the chain → data/ordo.db (:—) npm run gateway # protected RPC (:8547) npm run auction # auction, receipts, feed relay (:8548) npm run web # terminal, explorer, portal, API (:3000) npm test && npm run typecheck # production: Caddy in front of the services, one shared data volume docker compose -f deploy/docker-compose.prod.yml --env-file .env up -d --build
| Variable | Default | Purpose |
|---|---|---|
ORDO_RPC_URL · ORDO_RPC_URLS | sequencer RPC | Read upstream(s); comma-separated list rotates on transport failures |
ORDO_SEQUENCER_URL | https://rpc.mainnet.chain.robinhood.com | Where signed transactions are sent; third-party providers are reads only |
ORDO_API_KEYS | ephemeral dev key | key:label:rateLimit:rebateAddress:mode records |
ORDO_ALLOW_ANON · ORDO_ANON_RATE_LIMIT | 1 in production · 3000 | Anonymous wallet tier and its per-IP limit |
ORDO_AUCTION_URL · ORDO_AUCTION_TIMEOUT_MS | http://localhost:8548 · 3000 | Gateway → auction |
ORDO_AUCTION_WINDOW_MS · ORDO_HINT_LEVEL | 200 · pools | Auction window and hint disclosure |
ORDO_SETTLEMENT_ADDRESS · ORDO_AUCTIONEER_KEY | — | Enables bond checks, on-chain settlement and signed receipts |
ORDO_RECEIPT_LOG_ADDRESS · ORDO_RECEIPT_COMMIT_EVERY | — · 25 | Merkle anchoring |
ORDO_REBATE_USER/APP/PROTOCOL | 0.9 / 0.05 / 0.05 | Ledger split (on-chain split is in the contract) |
ORDO_FEED_URL | sequencer feed | Upstream for the relay |
ORDO_DB · ORDO_DATA_DIR | data/ordo.db · data/ | Index and persisted caches |
Compose services: caddy, gateway, auction, web, watcher, searcher (the house bidder that keeps the market non-empty), arb (the house arbitrage bot). Contracts are Foundry projects under contracts/; a Nitro full-node deployment for a bare-metal host is under deploy/nitro-node/.
Trust model & limitations#
- The sequencer sees everything
- Unavoidable on an L2 with a centralized sequencer. OrdoFi removes third parties from the path; it cannot remove the operator. Direct sequencer integration is the end of the roadmap for that reason.
- Bundles are best-effort
- Same-tick dispatch on a FCFS sequencer narrows the gap but does not guarantee adjacency. Use
OrdoExecutorfor atomicity. - Receipts have a bounded retraction window
- Everything up to
anchoredCountis immutable; the gap tocount(at most 25 receipts) could still be altered by a malicious operator. Anchoring cadence is a parameter. - The auctioneer key is hot
- It signs acks and receipts and calls
settle(). Its powers are bounded by searcher signatures (it cannot charge above a signed maximum, nor twice) and by the contract owner, who can rotate it withsetAuctioneer. - Contract owner
- The deployer owns OrdoSettlement and can change the auctioneer, the split and the treasury. Bonds and claimables cannot be seized by the owner; there is no owner withdrawal path.
- Hints leak early, by design
- Searchers learn pools and direction ~200 ms before the feed would show them. Amounts are withheld below the
fullhint level. This is the price of selling the backrun instead of giving it away. - Not audited
- The contracts are small and tested but have not had a third-party audit. Bond what you can afford to lose until they have.
- Not affiliated
- OrdoFi Labs is independent of Robinhood Markets, Inc.