OrdoFi Documentation

The execution layer for Robinhood Chain. 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

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();

Gateway — rpc.ordofi.network#

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_simulate600 req/min per source IPProtected direct send
Key · directApps without a rebate addressEverythingPer key, default 600/minProtected direct send
Key · auctionApps with a rebate addressEverythingPer key, default 600/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_sendRawTransaction ordo_simulate. Anything else without a key returns -32001.

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.

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
-32001unauthorized: valid x-api-key requiredMethod outside the anonymous tier and no valid 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 / 405GET 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 }

Order-flow auction — auction.ordofi.network#

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

OrdoSettlement#

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.
withdrawBond(uint256 amount)bond ownerReturns bond to the searcher
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 & OrdoExecutor#

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.

OrdoReceiptLog#

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 receipts#

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/feed#

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.

Apps & wallets#

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/sdk#

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/api#

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/tokensTradable 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/marketsEvery 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/resolversHealth of the persistent lookup caches: { pools, tokens, routes } each { known, pending }

MEV measurement#

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 · 600Anonymous 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_KEYEnables 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.