Documentation

Protected RPC on Robinhood Chain and Arc. Gateway, auction, settlement, receipts and the numbers behind them.

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.

Chain Robinhood Chain · Arbitrum Nitro L2 · id 4663Reference v0.1 · 2026-09Source github.com/OrdoFi/ordofi

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#

ParameterValue
ChainRobinhood Chain: Arbitrum Nitro (Orbit) L2 settling to Ethereum mainnet
Chain id4663 · 0x1237
Block time~0.1 s · single centralized sequencer, FCFS ordering, no public mempool
Gas tokenETH (18 decimals)
Sequencer RPChttps://rpc.mainnet.chain.robinhood.com
Sequencer feedwss://feed.mainnet.chain.robinhood.com
Explorerhttps://robinhoodchain.blockscout.com
WETH0x0bd7d308f8e1639fab988df18a8011f41eacad73
USDG0x5fc5360d0400a0fd4f2af552add042d716f1d168 (6 decimals)
Uniswap V3 factory / QuoterV2 / SwapRouter020x1f7d7550b1b028f7571e69a784071f0205fd2efa · 0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7 · 0xcaf681a66d020601342297493863e78c959e5cb2
Uniswap V4 PoolManager / PositionManager / StateView0x8366a39cc670b4001a1121b8f6a443a643e40951 · 0x58daec3116aae6d93017baaea7749052e8a04fa7 · 0xf3334192d15450cdd385c8b70e03f9a6bd9e673b, every V4 pool lives inside the singleton and is keyed by its 32-byte PoolId; native ETH is currency 0x0
OrdoFi hostServiceTransport
rpc.ordofi.networkGateway (JSON-RPC)HTTPS POST · GET / /health /metrics
auction.ordofi.networkAuction, receipts, feed relayHTTPS · WSS /searcher · WSS /feed
app.ordofi.networkTerminal, explorer, portal, this reference, public APIHTTPS · /api/* is CORS-open

Deployed contracts (chain 4663)#

ContractAddressRole
OrdoSettlement0xbC680922DaF2F65a8B957e5238857f8c68BeDabbSearcher bonds, second-price debit against a signed maximum, rebate accounting
OrdoBundler0xc0bccFb3aA4ad9160d272645376a1797a32f3c4aPer-owner CREATE2 executors for atomic multi-call bundles
OrdoReceiptLog0x89926c06cad403fDDD481C599b2ce709EBC936B9Merkle roots over all auction receipts ever issued
Auctioneer0x894255564bBb71585AebDf9337888b4b6E72C649Signs acks and receipts; the only address allowed to settle() and commit()
Protocol treasury0xf5ba571781533aaab9fc155311c93f5b1100affdReceives the protocol share of each settlement
OrdoPriceFeeds0x7DD461f4a56422E419C7F2d0cF44d58600F860d8Registry of the stock price feeds: one Chainlink-style feed per stock, market hours
OrdoExecutor0x8E303737059890abC216180bAEcd87b2622EEd75Runs 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.

NetworkChain IDRPCWebSocketExplorerGasWhat Ordo offers
Robinhood Chain
Robinhood's L2 (Arbitrum Nitro)
4663https://rpc.ordofi.networkwss://rpc.ordofi.networkblockscoutETHReads 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
2383https://aeva.ordofi.networkwss://aeva.ordofi.networkAevascanAEVAReads 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 testnet2382 testnethttps://aeva-testnet.ordofi.networkwss://aeva-testnet.ordofi.networkAeva explorerAEVA (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 mainnet
5042002 testnet
https://arc.ordofi.networkwss://arc.ordofi.networkarcscanUSDCReads 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:

TierWhoMethodsLimiteth_sendRawTransaction behaviour
AnonymousWallets, anyoneFull wallet set (see below) + ordo_simulate3,000 upstream reads + 60 sends per minute per source IPProtected direct send
Key · directApps without a rebate addressEverythingPer key, default 12,000/minProtected direct send
Key · auctionApps with a rebate addressEverythingPer key, default 12,000/minAuction 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.

PlanPer 30 daysIncludedRate limitOverageFor
Free$05M RU1,000/minhard stopA small app. Wallets need no key at all.
Builder$2950M RU12,000/min$5 / M RUA bot, a small terminal, one contract's indexer.
Pro$199500M RU60,000/min$3 / M RUIndexers and terminals in production; priority lane for log scans.
Dedicated$9995B RU250,000/min$1 / M RUA trading terminal's whole backend, and a person to call.
MethodRU
Every eth_*/net_* read not listed below, eth_getBlockByNumber/ByHash without transactions1
eth_feeHistory2
eth_estimateGas · eth_createAccessList · eth_getProof · ordo_simulate · eth_getLogs ≤ 100 blocks or by hash3
ordo_explain5
eth_getBlockByNumber/ByHash with full transactions6
eth_getLogs ≤ 1,000 blocks (or an open range) · ordo_quoteSwap10
eth_sendRawTransaction · ordo_sendRawTransaction · ordo_sendPrivateTransaction · ordo_sendBundle (per transaction)12
eth_getBlockReceipts13
eth_getLogs ≤ 10,000 blocks25
eth_simulateV1 · debug_traceTransaction · debug_traceCall30
eth_getLogs > 10,000 blocks · debug_traceBlockByNumber/ByHash50

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 with eth_call against latest state (the transaction's own gas limit is not forwarded into the simulation). A revert yields -32000 ordo: transaction would revert, not submitted: <reason> with data.ordoProtected = true. Otherwise the raw transaction is sent to the sequencer operator's endpoint and the hash is returned. Keys in auction mode route through the auction instead and receive result.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 deterministic OrdoExecutor address 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 an OrdoExecutor through OrdoBundler and execute the legs as one transaction with on-chain preconditions and a minGainWei guard.

Error codes#

CodeMeaningNotes
-32700parse errorBody is not JSON
-32601<method> is not available without an API key on this endpointAnonymous call to a keyed method; standard reads and sends are never affected
-32001unauthorized: the x-api-key presented is not validA key header was sent and it does not match any key
-32005rate limit exceeded, retry in NsPer key, or per IP for anonymous callers
-32000ordo: transaction would revert, not submitteddata.ordoProtected = true; also used for bundle refusals with data.failedIndex
-32602bundle.txs required · max 10 txs per bundleInvalid bundle params
-32000all RPC upstreams refused the requestEvery 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#

EndpointReturns
GET/Landing page with live probe and one-click wallet setup
GET/health{ status, upstream, sequencer, uptimeSeconds }
GET/metricsPrometheus 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.jsonSame, 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#

MainnetTestnet (served now)
Chain ID5042 · 0x13b25042002 · 0x4cef52
HTTPS RPChttps://arc.ordofi.network
WebSocketwss://arc.ordofi.network · eth_subscribe for newHeads and logs, plus ordinary calls on the same socket
Explorerarcscan.apptestnet.arcscan.app
Gas tokenUSDC, 18 decimals as the native currency
Block time~500 ms, final on confirmation
Health · metricsGET /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:

  1. The transaction is simulated from its recovered sender against current state. If it would revert, it is refused with -32000 and 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.
  2. 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.
  3. 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 ArcRobinhood Chain only, for now
Standard eth_* reads and eth_sendRawTransaction with simulation and private delivery
WebSocket subscriptions
ordo_simulate
Open 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_quoteSwap
Live 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:

  1. 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 with eth_simulateV1 to derive a hint.
  2. An opportunity is broadcast to every connected searcher. The window is ORDO_AUCTION_WINDOW_MS = 200 ms.
  3. 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.
  4. 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.
  5. 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.
  6. The auctioneer calls OrdoSettlement.settle() with the searcher's signature, debiting the bond and crediting user, app and protocol shares. A signed Receipt for 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:

LevelFields
minimalpoolsTouched, 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 bidWei wins.
  • Clearing price = second-highest bid; a lone bidder pays its own bid; no bids → no winner, price 0.
  • When ORDO_SETTLEMENT_ADDRESS is set, a bid is only accepted if OrdoSettlement.bond(searcher) ≥ bidWei (bond reads cached ORDO_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 bytes32 by stripping dashes and right-padding with zeros.

Rebate split#

RecipientShareWhere it landsEnv
User (transaction sender)90%claimable[user] in OrdoSettlementORDO_REBATE_USER=0.9
App (order-flow originator)5%claimable[rebateAddress]ORDO_REBATE_APP=0.05
Protocol5%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#

EndpointReturns
POST/submitAuction 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 · /feedSee 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.

FunctionAccessSemantics
payabledeposit() · receive()anyoneAdds msg.value to bond[msg.sender]. A plain ETH transfer to the contract bonds the sender.
requestWithdraw(uint256 amount) · cancelWithdraw() · withdrawBond()bond ownerLeaving 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)viewbond less any pending withdrawal, what the auctioneer accepts bids against
settle(Settlement s, bytes searcherSig)auctioneer onlyVerifies 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()anyonePays out claimable[msg.sender]
bond(address) · claimable(address) · settled(bytes32)viewBalances and replay state
bidDigest(address, bytes32, uint256)viewThe exact EIP-712 digest a searcher signs; useful for wallets and audits
DOMAIN_SEPARATOR() · BID_TYPEHASH() · auctioneer() · protocolTreasury() · appBps() · protocolBps() · owner()viewParameters
setAuctioneer · setSplit · setProtocolTreasury · transferOwnershipowner onlyAdministration. 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.

FunctionSemantics
OrdoBundler.executorOf(address owner) → addressDeterministic 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[]) → boolEvaluate 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#

FunctionSemantics
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) → boolInclusion 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#

ClaimCheck
My bid was countedThe acknowledged (searcher, bidWei, receivedAt) appears in the receipt's bids
No bids were inventedEvery bidSig recovers to its searcher under the OrdoSettlement domain
Second price was honouredwinner is the highest bidder; clearingPriceWei equals the second-highest bid (or the sole bid)
The receipt is the auctioneer'ssignature recovers to the published auctioneer under the OrdoAuction domain
It cannot be swapped laterreceiptHash 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 answerMeaning
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, approvalFor an EOA: the swap alone, and the approval to send first if any.
amountOut, minOutThe 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, appFeeWhat 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, reclaimHow 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.
simulationWith 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.
expiresAtQuotes move; ask again after this.
LatencyAbout 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 statusMeaning
statusqueued (working on it), submitted (a transaction is out), confirmed, or a final expired / cancelled / failed. The last three mean nothing moved.
waitingWhat a queued order is waiting for, in words: the price to come back to the minimum, the owner's balance or approval.
resulttx, 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, recoveriesEvery action taken, with times and transaction hashes, and how many of them were recoveries.
txsEvery transaction signed for this order. Only one of them can have run it.
GET /v1/statsOrders, 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 happensWhat Ordo does
The price is under the user's minimumWaits 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 chainNothing moved: the signature is spent only on success. Routes again, simulates again, resends.
A transaction is not picked upSends 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 replacedUntil 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 signatureA balance or Permit2 approval that is short keeps the order waiting, not failed, until the deadline.
An endpoint failsReads 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.
RegistryReturns
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#

TopicWhat to do
FreshnessupdatedAt 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 gapprices().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 hoursStock 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.
DepthA 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.
ChangesA 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 whatThe 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 answerMeaning
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.txThe 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.
simulationWith 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.
filledByWho 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 }
ThenWhat happens
The user sendsA 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 amountRe-priced at the rate when it arrives and filled, as long as it still covers fees. flexibleAmount says so.
It cannot be filledRefunded 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? }. Returns 201 { key, label, mode, rebateAddress, rateLimitPerMin, note, rpc }. With a rebate address the key is auction mode; without, direct (protected passthrough only). Limited to 3 keys per hour per IP. Keys look like ordo_ + 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. Exposes address.
onOpportunity(opp, { account }) → Promise<{ maxBidWei: bigint, backrunRawTx: Hex } | null>
Called per opportunity with opp = { id, hint: { poolsTouched, to, selector, value }, originLabel }. Return null to pass. A returned decision is signed (signBid) and sent as a bid frame with bidWei = maxBidWei.
searcher.signBid(opportunityId, maxAmountWei) → Promise<Hex>
EIP-712 Bid signature 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.

EndpointReturns
GET/api/statsEmbed-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/reportThe watcher's full MEV report: venues, pools, searchers, USD totals
GET/api/arbs/recent?n=40Latest 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/auctionProxy of the auction's /stats
GET/api/explorer{ report, recentArbs, auction, onchain, generatedAt }, everything the explorer page renders
POST/api/keys · GET/api/accountSee 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.

EndpointParamsReturns
/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/tokenaddressImport any ERC-20 by address: resolves metadata and checks ETH/USDG routability. Tradable imports join the list.
/api/trade/quotetokenIn, 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/candlesbase, quote, bucketSec?=60, spanBlocks?=72000OHLCV 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/pairbase, quote{ pool, fee, tvlUsd, reserves, base, quote } for the deepest direct pool by in-range liquidity; cached 60 s
/api/trade/tradesbase, quote, limit?=40Most recent individual swaps: { block, time, tx, logIndex, price, sizeBase, sizeQuote, side }; cached 5 s
/api/trade/balancesaddressNative 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's from and to only (not arbitrary recipients), and native value counts 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 profitWei in 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
VariableDefaultPurpose
ORDO_RPC_URL · ORDO_RPC_URLSsequencer RPCRead upstream(s); comma-separated list rotates on transport failures
ORDO_SEQUENCER_URLhttps://rpc.mainnet.chain.robinhood.comWhere signed transactions are sent; third-party providers are reads only
ORDO_API_KEYSephemeral dev keykey:label:rateLimit:rebateAddress:mode records
ORDO_ALLOW_ANON · ORDO_ANON_RATE_LIMIT1 in production · 3000Anonymous wallet tier and its per-IP limit
ORDO_AUCTION_URL · ORDO_AUCTION_TIMEOUT_MShttp://localhost:8548 · 3000Gateway → auction
ORDO_AUCTION_WINDOW_MS · ORDO_HINT_LEVEL200 · poolsAuction 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— · 25Merkle anchoring
ORDO_REBATE_USER/APP/PROTOCOL0.9 / 0.05 / 0.05Ledger split (on-chain split is in the contract)
ORDO_FEED_URLsequencer feedUpstream for the relay
ORDO_DB · ORDO_DATA_DIRdata/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 OrdoExecutor for atomicity.
Receipts have a bounded retraction window
Everything up to anchoredCount is immutable; the gap to count (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 with setAuctioneer.
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 full hint 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.