Last 24hassertions verified316discrepancies0feed lead p50+836mschain head21,965,794rollup0x23A19d…2D94
Layer two · specification

A Flashbots-style bundle cannot exist on this chain.

That is the first thing worth knowing and it is not a soft constraint. Robinhood Chain has one sequencer, no public mempool, strict first-come-first-served ordering, no block builder and no auction. There is no party anywhere in the pipeline whose job is to permute the contents of a block, which is the entire mechanism a Flashbots bundle relies on.

Say this plainly before anything else is sold

eth_maxPriorityFeePerGas returns 0x0 on mainnet. There is nothing to bid into. Timeboost is not enabled. There is no eth_subscribe, no mempool to watch and no debug_* or trace_* namespace. Ordering is decided by the moment a transaction reaches the sequencer socket and by nothing else.

Any product sold on this chain as ordering priority, a guaranteed position inside a block, immunity from a faster participant, or a shield against MEV is fraud. Not exaggeration — fraud, because the mechanism it describes does not exist and cannot be built without the operator changing the sequencer. Verderer sells none of those things, at any tier, at any price.

What was actually being bought

A bundle is three separable products sold under one word.

Ask a sniper what a bundle does for them and the answer is always some mixture of three guarantees. They are usually bought together, but they are not one thing, and only the third of them needs a builder. Two survive on this chain intact. The third does not survive at all.

01obtainable

Atomicity

All of my actions land, or none of them do. No partial fill, no half-executed arbitrage leg, no approval granted into a trade that never happened.

Needs no reordering. Collapse N actions into one transaction and the EVM enforces it — a stronger guarantee than a builder promising to include a bundle it might drop.

02obtainable

Conditionality

Do not execute unless the chain still looks the way it looked when I decided to act. A reserve moved, a slot flipped, a block passed — then I want out, cheaply.

Needs no reordering either. It is enforced twice: by the sequencer at admission for free, and by guards inside the executor at execution for gas.

03not obtainable

Position

Put me ahead of that other transaction. Back-run this swap, land first in the block, be the one who gets the fill.

This is the only one that requires permuting a block, and it is the only one nobody on this chain can sell. What is left of it is a latency race, decided by arrival time, which we compete in and do not control.

Everything below is the specification for 01 and 02. Nothing below is a workaround for 03, because a workaround for 03 would be a lie.

modeatomicone sender · N calls · one transaction

Atomicity you already have. It just has to be spent in one transaction.

An atomic bundle here is a single signed transaction sent to a VerdExecutor contract that you own. It carries an ordered list of calls, and the EVM does the rest: if any call reverts, the whole transaction reverts and no state survives. That is not a promise we make. It is the machine’s, and it is strictly stronger than a builder’s promise of inclusion, which can be broken by the builder losing the block.

What the contract adds on top of raw multicall is refusal. Pre-guards read live state before anything executes, post-guards read it after, and balance checks bound the whole bundle in the only terms a trader actually cares about: how much more of a token the account holds at the end than at the start.

VerdExecutor — the bundle type
enum GuardOp { Eq, Neq, Gte, Lte }

struct Call {
    address target;
    uint256 value;
    bytes   data;
    bool    allowFailure;    // false on every call is what makes it atomic
}

struct Guard {
    address target;          // staticcall this
    bytes   data;            //   with this calldata
    uint256 wordIndex;       //   take the 32-byte word at this index of the return
    GuardOp op;              //   compare it
    uint256 value;           //   against this
}

struct BalanceCheck {
    address token;           // address(0) = native ETH
    address account;         // address(0) = this executor
    uint256 minGain;         // require(after - before >= minGain)
}

struct Bundle {
    Call[]         calls;
    Guard[]        preGuards;
    Guard[]        postGuards;
    BalanceCheck[] balanceChecks;
    uint64         deadline;        // unix seconds; 0 disables
    uint64         maxBlockNumber;  // inclusive; 0 disables
}

function execute(Bundle calldata bundle)
    external payable onlyAuthorized nonReentrant
    returns (bytes[] memory results);

function simulate(Bundle calldata bundle) external payable;  // always reverts, returns (bool[], bytes[])

Guards: a staticcall and a comparison

A guard staticcalls target with data, takes the 32-byte word at wordIndex of the return, and compares it against value under Eq / Neq / Gte / Lte. A short return or a failed staticcall is a revert, not a pass — there is no fail-open path. That is the whole mechanism, and it is deliberately small: anything expressible as “read a word, compare it” is expressible as a guard, including reserves, prices from an oracle view, allowances, and pool state.

Pre-guards run before the first call. Post-guards run after the last one. Both are view, so a guard cannot itself move the chain.

Balance deltas: the guard that protects a trader

A BalanceCheck snapshots a balance before the calls and requires after - before >= minGain afterwards. It does not care how execution went wrong — a bad route, a fee-on-transfer token, a pool that moved between simulation and execution — only that the bundle may not end holding less than it committed to holding. A decrease reports as a gain of zero, which is the honest description of that outcome, and it reverts.

Use maxBlockNumber, not deadline, for anything tight. Blocks arrive about every 100 ms here (measured p50 76 ms) while timestamps advance in whole seconds, so a timestamp bound cannot separate adjacent blocks.

Why one executor per owner, and never a shared singleton

A shared contract that performs arbitrary calls on behalf of whoever asks is a standing trap. The moment any user grants it an ERC-20 allowance, anybody else can call it with transferFrom(victim, attacker, amount) as one of their calls and drain that allowance. This is not hypothetical; it is one of the most repeatedly exploited bug classes in the space, and documenting a shared router as “unsafe to approve” is not a fix, because approving a router is exactly what users do.

So each account is an EIP-1167 minimal-proxy clone owned by exactly one address, and only the owner or an operator the owner authorised may call execute. An approval to your executor is therefore as safe as an approval to your own wallet. The clone address is derived deterministically from the owner, so you can compute it, approve tokens to it, and only pay for the deployment when you place your first bundle — pre-approving an address nobody else can ever occupy is safe by construction.

VerdExecutorFactory — deterministic addressing
function predict(address owner) external view returns (address);
function ensure(address owner)  external returns (address executor);  // deploy-or-return
function executorOf(address owner) external view returns (address);
function isDeployed(address owner) external view returns (bool);
salt = bytes32(uint256(uint160(owner))). The relay calls ensure() on the hot path so a first-time submitter is not a special case.
VerdExecutor — hot keys without withdrawal rights
function setOperator(address operator, bool allowed) external onlyOwner;
function transferOwnership(address newOwner)        external onlyOwner;
function sweep(address token, address to, uint256 amount) external onlyOwner;
An operator may execute bundles. It may not sweep funds, add operators or transfer ownership. A key that can trade should not also be a key that can withdraw.
modesettlementN signers · one transaction · atomic across all of them

The only genuine multi-party bundle available here — and it needs nothing from the sequencer.

The executor solves atomicity for one sender. It cannot help two people who each need to send their own transaction, because nothing can make two separately sequenced transactions atomic on a chain that will not reorder. The way out is the one CoW Protocol and UniswapX already took, years before this chain existed: stop trying to bundle transactions and bundle intents instead.

Each participant signs an EIP-712 Intent off-chain and never sends a transaction at all. A solver collects the signatures and submits one transaction that pulls every maker’s funds through Permit2, routes them however it likes, and pays each maker out. If any leg fails the whole settlement reverts and nobody moved. That is a real N-party atomic bundle, and it requires no cooperation from the sequencer whatsoever — which is precisely why it works here.

Replay protection is Permit2’s unordered nonce bitmap rather than a mapping in the settlement contract. One less place to get it wrong, and it lets a maker cancel by invalidating a nonce directly on Permit2 without the settlement contract needing to be alive or cooperative.

VerdSettlement — the intent type
struct Intent {
    address maker;
    address sellToken;
    address buyToken;
    uint256 sellAmount;
    uint256 minBuyAmount;   // the maker's limit price; see the note on surplus
    uint256 nonce;          // Permit2 unordered nonce, and the cancellation handle
    uint256 deadline;
}

struct SolverCall { address target; uint256 value; bytes data; }

function settle(
    Intent[]     calldata intents,
    bytes[]      calldata signatures,   // Permit2 witness sigs, index-aligned with intents
    SolverCall[] calldata solverCalls,  // arbitrary routing between pull and payout
    address[]    calldata sweepTokens
) external payable;

function witnessFor(Intent calldata intent) external pure returns (bytes32);

// EIP-712 primary type
"Intent(address maker,address sellToken,address buyToken,"
"uint256 sellAmount,uint256 minBuyAmount,uint256 nonce,uint256 deadline)"
Permit2 and the settlement contract itself are forbidden as solver-call targets: the first would let a solver reach for allowances outside this settlement, the second would let it re-enter around the payout accounting.
Surplus goes to the solver. Stated here, not buried.

Makers receive exactly the minBuyAmount they signed — the limit price they chose, never worse. Anything the solver extracts above that is the solver’s margin.

That is not a leak, it is the incentive: margin is what makes solvers compete to find better routes, and a market with no solver margin has no solvers. A maker who wants more of the surplus should sign a higher minBuyAmount and accept a lower fill probability. There is no third option and anyone offering one is describing a subsidy, not a mechanism.

What settlement cannot do

Every party must have signed already. There is no mechanism, here or on any chain, to include an unwilling or unaware counterparty in a settlement. A “bundle” that claims to sandwich or back-run someone is describing ordering, which is item 03, which does not exist here.

Payouts are checked against the contract’s live balance at payout time, so a solver cannot pay maker A out of funds owed to maker B and leave the last maker short. The shortfall surfaces as a revert rather than as a silent underpayment.

modeburstnot atomic2 to 16 pre-signed transactions · sequential nonces · one sender

The mode that comes with a warning attached to every response.

Burst takes N pre-signed transactions from one sender with sequential nonces and writes them back-to-back on an already-warm connection to the sequencer, without awaiting the round trip in between — the gap between writes is what decides whether they land together, and awaiting each response would insert a full round trip between consecutive transactions.

Under first-come-first-served, sequential nonces written this way usually land contiguously. Usually is the honest word. A foreign transaction can land between them and a block boundary can split them, and when that happens you have executed the first half of something and not the second half. There is no rollback.

Use burst when the legs are independently safe — several separate fills, several separate approvals — and use atomic when they are not. If a partial execution would hurt you, burst is the wrong tool and no measured contiguity rate makes it the right one.

Contiguity is measured and published, never promised

Every burst response carries this string verbatim:

“Burst mode is NOT atomic. Sequential nonces from one sender usually land contiguously under FCFS, but a foreign transaction or a block boundary can split them. See /v1/relay/stats for the measured contiguity rate.”

The rate below is that measurement, from this process, on this node. It is not a service level and it carries no remedy.

observed contiguityno datano bursts submitted yet
bursts submitted0this process
The part that matters most

Conditional submission turns a paid failure into a free one.

On a chain with a fee auction, a failed transaction at least bought you a lottery ticket. Here there is no auction — eth_maxPriorityFeePerGas is 0x0 — so a revert is pure deadweight loss with nothing whatsoever to compensate it. That single fact is why this section exists, and why conditions are on by default rather than an advanced option.

eth_sendRawTransactionConditional is live on Robinhood Chain — verified against mainnet, not inferred from the Nitro source. It lets the submitter attach preconditions to a raw transaction, and the sequencer itself evaluates them at admission. If the state you simulated against has already moved, the transaction is rejected before it is included, and you are charged nothing. It never enters a block, so there is no gas to pay.

This buys no ordering privilege — not queue position, not a place inside a block. A conditional transaction is sequenced exactly like an unconditional one. What it buys is the right to not be sequenced at all, for free, when the trade has already stopped making sense — which on this chain is the difference between a strategy that survives a bad day and one that bleeds gas into a market that will not pay it back.

You do not have to construct the conditions yourself. When a bundle is simulated first — the default — the relay records which accounts and slots the simulation read and derives knownAccounts from exactly that set. The common case gets free revert protection without the caller thinking about it. Supplying conditions explicitly overrides the derivation.

Conditional options — the shape accepted by POST /v1/bundles
"conditional": {
  // address -> storage root hash, or address -> { slot: value }
  "knownAccounts": {
    "0x8bcEaA40B9AcdfAedF85AdF4FF01F5Ad6517937f":
      "0x4a1f0e37c58b90d2a6c1f8b3e07d4425cbb6f1a908e37d05c4a2be7719f3d84c",
    "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73": {
      "0x0000000000000000000000000000000000000000000000000000000000000008":
        "0x000000000000000000000000000000000000000000000000016345785d8a0000"
    }
  },
  "blockNumberMin": "0x1e8f2be",   // hex quantity
  "blockNumberMax": "0x1e8f2c4",   // hex quantity
  "timestampMin": 1785000000,      // unix seconds
  "timestampMax": 1785000012
}
Every field is optional. An empty or absent object sends the transaction through eth_sendRawTransaction instead; any non-empty object switches the relay to eth_sendRawTransactionConditional.

Prefer blockNumberMax over timestampMax. At roughly 100 ms per block against whole-second timestamps, a timestamp bound that looks tight spans about ten blocks.

Rejection strings, as the sequencer returns them

Error returnedWhat movedCost to youStatus
Storage root hash condition not metthe storage root of an account named in knownAccounts changedzero gasobserved on mainnet
BlockNumberMax condition not metthe chain moved past the block you were willing to land inzero gasobserved on mainnet
Storage slot value condition not meta specific slot named in knownAccounts changedzero gasrecognised
BlockNumberMin condition not metsubmitted too earlyzero gasrecognised
TimestampMin condition not metsubmitted before the window openedzero gasrecognised
TimestampMax condition not metsubmitted after the window closedzero gasrecognised

The relay classifies any of these as rejectedByPrecondition rather than as a failure, because from the caller’s side they are the mechanism working. The two marked as observed were returned by the live sequencer during testing; the rest are recognised by the relay and have not yet been triggered in production.

Conditions are checked at admission, not at execution
A residual window remains between the sequencer accepting your transaction and executing it. The executor’s on-chain guards close that window authoritatively, at the cost of gas when they fire. Use both layers and understand which is which: the free layer is the weaker one, and the layer that actually cannot be beaten is the one you pay for when it saves you.
Interface

The API

Three endpoints. Bearer-token auth, pro tier and above for submission and simulation, and per-tier rate limits published in the table at the end of this section. Base URL is the relay host you were issued. The shapes below are transcribed from the request schemas rather than written for the page; the values in them are illustrative, and every measured figure on this page lives in the telemetry section.

POST /v1/bundlesatomic
request
{
  "mode": "atomic",
  "rawTransaction": "0x02f8b4821237...c0808080",
  "simulateFirst": true,
  "conditional": {
    "knownAccounts": {
      "0x8bcEaA40B9AcdfAedF85AdF4FF01F5Ad6517937f":
        "0x4a1f0e37c58b90d2a6c1f8b3e07d4425cbb6f1a908e37d05c4a2be7719f3d84c"
    },
    "blockNumberMax": "0x1e8f2c4"
  }
}
response — accepted
{
  "bundleId": "bnd_9f31c0a4d78b26e5104f8a3c",
  "mode": "atomic",
  "status": "submitted",
  "transactionHashes": ["0x7d0c...41ab"],
  "sequenceNumber": null,
  "blockNumber": null,
  "inclusionLatencyMs": null,
  "contiguous": null,
  "gasUsed": null,
  "error": null,
  "simulation": { "ok": true, "calls": [], "blockNumber": "...", "error": null },
  "createdAt": "...",
  "updatedAt": "..."
}
response — rejected by precondition, no gas spent
{
  "bundleId": "bnd_2b7ae5019c4d83f60a1e7d55",
  "mode": "atomic",
  "status": "rejected",
  "transactionHashes": ["0x7d0c...41ab"],
  "error": "Storage root hash condition not met",
  "sequenceNumber": null,
  "blockNumber": null,
  "inclusionLatencyMs": null,
  "gasUsed": null,
  "createdAt": "...",
  "updatedAt": "..."
}
status rejected with one of the six condition strings means the transaction never entered a block. It is counted under rejectedByPrecondition in the telemetry below.
POST /v1/bundlesburst
request — 2 to 16 raw transactions, sequential nonces, one sender
{
  "mode": "burst",
  "rawTransactions": [
    "0x02f8b4821237...8080",
    "0x02f8b4821237...8081",
    "0x02f8b4821237...8082"
  ]
}
response — note the warning field
{
  "bundleId": "bnd_c40d19e73a5b8f2016cc4de1",
  "mode": "burst",
  "status": "submitted",
  "transactionHashes": ["0x...", "0x...", "0x..."],
  "contiguous": null,
  "error": null,
  "warning": "Burst mode is NOT atomic. Sequential nonces from one sender
              usually land contiguously under FCFS, but a foreign
              transaction or a block boundary can split them. See
              /v1/relay/stats for the measured contiguity rate.",
  "createdAt": "...",
  "updatedAt": "..."
}
contiguous stays null until every leg has been seen in the feed. It resolves to true only if all of them landed in the same block.
GET /v1/bundles/:id
response — the persisted record, after inclusion
{
  "id": "bnd_9f31c0a4d78b26e5104f8a3c",
  "mode": "atomic",
  "status": "included",
  "transactionHashes": ["0x7d0c...41ab"],
  "sequenceNumber": "...",
  "blockNumber": "...",
  "inclusionLatencyMs": "...",
  "contiguous": null,
  "simulation": { "ok": true, "calls": [] },
  "conditional": { "blockNumberMax": "0x1e8f2c4" },
  "error": null,
  "updatedAt": "2026-07-25T00:00:00.000Z"
}
Inclusion is detected from the sequencer feed rather than by polling for a receipt, which is what makes this status resolve ahead of the public RPC. A bundle never seen within 60 seconds resolves to status expired rather than remaining pending forever.
POST /v1/simulate1 to 32 calls
request
{
  "from": "0x1f4b9c07e5a8d3620fb1c8a4e9d70532cb814a6d",
  "calls": [
    {
      "to": "0x8876789976dEcBfCbBbe364623C63652db8C0904",
      "data": "0x3593564c0000...",
      "value": "0"
    },
    {
      "to": "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
      "data": "0x70a08231000000...",
      "value": "0"
    }
  ]
}
response
{
  "ok": true,
  "calls": [
    { "ok": true, "gasUsed": "...", "returnData": "0x...", "revertReason": null },
    { "ok": true, "gasUsed": "...", "returnData": "0x...", "revertReason": null }
  ],
  "blockNumber": "...",
  "freshnessMs": "...",
  "totalGasUsed": "...",
  "error": null
}
Backed by eth_simulateV1, which is live on this chain and gives per-call results plus state overrides in one round trip. There is no debug_traceCall here, so this is the only multi-call simulation path that exists. Standard Error(string) reverts are decoded into revertReason.

A simulation is a forecast against the freshest state we hold, which is fresher than the public RPC will serve. State still changes between the simulation and the execution, and no indemnity attaches to a simulation result.

Rate limits, by tier

Tierbundles / minsimulate / minfeed delay
Pro60600real time
Turbo6006,000real time
Colocated6,00060,000real time

Rate limits are throughput, not priority. A higher tier submits more often; it never submits earlier than someone else who pressed the key first. Access tiers →

Contracts

ContractRoleAddress
VerdExecutorFactorydeterministic per-owner clonesnot deployed
VerdExecutorimplementation behind the clonesnot deployed
VerdSettlementN-signer intent settlementnot deployed
Permit2maker fund pull, unordered nonces0x000000000022D473030F116dDEE9F6B43aC78BA3
Measured, not asserted

Relay telemetry

Counters from the running relay process. Nothing here is cumulative marketing history and nothing here is rounded: a figure the node has not produced renders as absent rather than as a zero.

bundles accepted0handed to the sequencer
rejected free of charge0preconditions caught them pre-inclusion
rejected in preflight0simulation reverted, never submitted
included0confirmed via the feed
awaiting inclusion0submitted, not yet seen

Inclusion latency — submit to seen in feed

PercentileValue
samplesnone yet
minno data
p50no data
p90no data
p99no data
maxno data
meanno data

Measured from the moment the relay writes to the sequencer socket to the moment the transaction appears in the sequencer feed. It is a measurement of this node’s path to the sequencer, not a promise about yours.

Burst contiguity

contiguity rateno dataall legs in one block
bursts contiguous0
bursts total0

Published as an observation with its sample size attached, so a rate computed from a handful of bursts cannot be mistaken for a rate computed from thousands. Price the failure case in regardless of what this reads today.

Live JSON: GET /v1/relay/stats

Before you build against any of this

What none of it does

The condensed version. The full list, including the parts that have nothing to do with bundles, is kept as a first-class page rather than a footnote.

  1. 01

    No ordering priority, at any tier or price.

    The sequencer orders strictly by arrival. eth_maxPriorityFeePerGas returns 0x0, Timeboost is off, and nothing in this specification moves a transaction ahead of another one.

  2. 02

    No protection from a faster participant.

    Bundles do not hide you — the absent public mempool already does that, equally, for everyone. Lower latency wins, we can lower yours, and we sell the same service to whoever you are racing.

  3. 03

    Atomicity is per transaction and nothing else.

    Anything spanning two or more transactions from the same sender cannot be made atomic here by us or by anyone. burst is explicitly non-atomic and its response payload says so in full.

  4. 04

    Guards prevent bad execution. They do not produce good execution.

    A guarded bundle that would fill badly does not fill. You get nothing and somebody else may get the trade. Conditional submission makes that outcome free rather than costly, which is the best available result and not a win.

  5. 05

    Gas is charged when an on-chain guard fires.

    If a bundle passes admission but its executor guards fail at execution, it reverts and consumes gas, with no auction to compensate you. That is exactly why simulation is on by default and why conditions are derived from what the simulation read.

  6. 06

    The relay is a trusted intermediary, and so is the sequencer.

    Submitting through us reveals your intent before it is on-chain. Against that we offer published latency telemetry, signed ingress receipts, and a documented option to bypass us entirely and use the public RPC. What we cannot offer is a cryptographic guarantee against relay or sequencer misbehaviour; that needs an encrypted mempool and none is deployed.

  7. 07

    Censorship resistance exists, but it is not an execution path.

    Force inclusion through the delayed inbox takes a flat 4 days. Against a 100 ms block time that is worthless for trading. We monitor it as a backstop and will never present it as a fallback route for a bundle.

This page is written as a specification rather than a pitch because the failure mode of the alternative is specific and expensive: a customer who believes they bought ordering, sizes a position accordingly, and discovers on the first contested launch that the word meant something else. Everything here is checkable against the contracts and against the relay endpoints. Where a claim could not be checked, it is not made.