From 5b57a7ee707a57f983a2a3214986a6739e39d089 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Mon, 13 Jul 2026 15:05:00 +0200 Subject: [PATCH 01/89] robinhood chain: chains hub entry, chain-kpis, l2-block-time (HTTP poll mode), network-fees --- benchmarks/l2-block-time.yml | 21 +++- benchmarks/network-fees.yml | 22 +++- harnesses/chain-kpis/cmd/script/registry.go | 1 + harnesses/l2-block-time/cmd/script/config.go | 12 ++ .../cmd/script/sequencer_poll.go | 106 ++++++++++++++++++ .../l2-block-time/cmd/script/sequencer_ws.go | 7 +- .../transaction-fee/cmd/script/config.go | 9 ++ src/lib/chains.ts | 8 ++ 8 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 harnesses/l2-block-time/cmd/script/sequencer_poll.go diff --git a/benchmarks/l2-block-time.yml b/benchmarks/l2-block-time.yml index 17199113..e65d3f81 100644 --- a/benchmarks/l2-block-time.yml +++ b/benchmarks/l2-block-time.yml @@ -2,12 +2,16 @@ slug: l2-block-time number: "009" -title: Fastest L2 block time, live across Arbitrum, Base, Optimism and 6 more +title: Fastest L2 block time, live across Robinhood, Arbitrum, Base and 7 more seo_title: "Fastest L2 block time 2026" -seo_description: "Fastest L2 sequencer block time live: Arbitrum, Blast, Optimism, Base, Linea and more ranked by newHeads interval." +seo_description: "Fastest L2 sequencer block time live: Robinhood Chain, Arbitrum, Optimism, Base, Linea and more ranked by real block interval." subtitle: Wall-clock interval in milliseconds between two consecutive newHeads events on each L2 sequencer, refreshed continuously. per_chain_explainer: + - slug: robinhood + h2: "Robinhood Chain block time" + body: | + Robinhood Chain block time is {{p50:robinhood}} (p50, 24h), the most aggressive cadence in the cohort: the Arbitrum Orbit stack lets Robinhood's sequencer target ~100 ms blocks for tokenized-stock settlement. The chain launched mainnet on July 1, 2026, pays gas in ETH and settles to Ethereum blobs. Measurement note: Robinhood exposes no public WebSocket, so block time here is derived from block-number deltas over 2-second HTTP poll windows, an accurate average that does not capture per-block jitter. - slug: arbitrum h2: "Arbitrum One block time" body: | @@ -66,6 +70,7 @@ abstract: | outlier sample is capped at 5 min so a reconnect can't pollute p50. methodology: + - "Robinhood Chain exception: the sequencer exposes no public WebSocket, so its block time is derived from eth_blockNumber deltas polled every 2 seconds over HTTP. A window that advanced N blocks contributes N samples of (elapsed / N) ms, which preserves per-block weighting and yields an accurate average; per-block jitter is not captured on this chain and this note is the disclosure." - "Cadence: continuous WebSocket subscription per L2, one persistent connection, no polling." - "Subscribe payload: `{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_subscribe\",\"params\":[\"newHeads\"]}`. No API key, public endpoints (publicnode.com mostly, era.zksync.io/ws for zkSync)." - "Sample: block-time(N) = T_recv(N) − T_recv(N-1), measured in milliseconds with `time.Now()` precision on the harness clock. Block-number duplicates from the same connection are filtered." @@ -101,6 +106,18 @@ faq: a: "Polygon zkEVM and Mode don't expose a public WebSocket endpoint without an API key, so we defer them to v2 when an API-keyed contributor endpoint lands. Starknet uses the Cairo stack with a different RPC shape, it would live in a dedicated bench rather than be shoehorned into the EVM cohort here. Berachain, Sonic and other adjacent chains are L1s, not L2s, so they live in the l1-finality bench." providers: + - slug: robinhood + name: Robinhood Chain + tag: Arbitrum Orbit, ~100ms blocks, HTTP-polled (no public WSS) + formula: "Median milliseconds per block on Robinhood Chain, derived from eth_blockNumber deltas over 2s HTTP poll windows (no public WebSocket), p50 over 24h." + queries: + p50: histogram_quantile(0.50, sum(rate(l2_block_time_milliseconds_histogram_bucket{chain="robinhood"}[24h])) by (le)) + p90: histogram_quantile(0.90, sum(rate(l2_block_time_milliseconds_histogram_bucket{chain="robinhood"}[24h])) by (le)) + p99: histogram_quantile(0.99, sum(rate(l2_block_time_milliseconds_histogram_bucket{chain="robinhood"}[24h])) by (le)) + mean: sum(rate(l2_block_time_milliseconds_histogram_sum{chain="robinhood"}[24h])) / sum(rate(l2_block_time_milliseconds_histogram_count{chain="robinhood"}[24h])) + success: avg_over_time(l2_block_time_health{chain="robinhood"}[24h]) + sample_size: sum(increase(l2_block_time_samples_total{chain="robinhood"}[24h])) + series: avg_over_time(l2_block_time_milliseconds{chain="robinhood"}[1h]) - slug: arbitrum name: Arbitrum One tag: Nitro fast-finality, sub-second sequencer diff --git a/benchmarks/network-fees.yml b/benchmarks/network-fees.yml index 36e65e3a..730f3900 100644 --- a/benchmarks/network-fees.yml +++ b/benchmarks/network-fees.yml @@ -2,10 +2,10 @@ slug: network-fees number: "031" -title: Cheapest blockchain transaction fee, live across 14 L1 and L2 chains +title: Cheapest blockchain transaction fee, live across 15 L1 and L2 chains seo_title: "Cheapest crypto transaction fee 2026" -seo_description: "Cheapest blockchain transaction fee in USD live across 14 chains. Solana, Base, Arbitrum, BNB Chain ranked." -subtitle: "Live USD cost of one native token transaction on 14 Layer 1 and Layer 2 chains, refreshed every 30 seconds." +seo_description: "Cheapest blockchain transaction fee in USD live across 15 chains. Solana, Base, Robinhood Chain, Arbitrum, BNB ranked." +subtitle: "Live USD cost of one native token transaction on 15 Layer 1 and Layer 2 chains, refreshed every 30 seconds." seo_intro: | This page answers one question. How much does it cost in dollars to send one transaction on each major blockchain right now. We track 14 chains in parallel and refresh the number every 30 seconds. The eight Layer 1 chains are Ethereum, Solana, BNB Chain, TRON, Cardano, Sui, Litecoin and Monero. The six Layer 2 rollups are Arbitrum, Base, zkSync Era, Linea, Mantle and Taiko. For every chain we query its own fee market directly (eth_feeHistory for the EVM family, getRecentPrioritizationFees on Solana, koios epoch params on Cardano, get_fee_estimate on Monero, the mempool oracle on Litecoin, getChainParameters on TRON, suix_getReferenceGasPrice on Sui), convert the result to the smallest native unit, then multiply by the live USD price of the chain's native token from Mobula. The output is the actual dollar amount a wallet user pays today. No gwei to lamport conversion, no marketing claim. Compare Ethereum gas now versus Solana fee in USD, see whether Arbitrum is still cheaper than Base today, find out which Layer 1 has the lowest transaction cost this minute. @@ -71,6 +71,10 @@ per_chain_explainer: h2: "Arbitrum transaction fee" body: | Arbitrum One native transfer fee is {{p50:arbitrum}} (p50, 24h). Arbitrum Nitro runs an EIP-1559 fee market against the sequencer at sub-second cadence; native ETH transfers are 21000 gas and sit at fractions of a cent because the sequencer's priority fee is near-zero by design. The L1 data-posting component (Ethereum blobs since Dencun) is not yet included in this figure, only L2 execution cost. Computed via `eth_feeHistory` on the Arbitrum sequencer RPC times live ETH price. + - slug: robinhood + h2: "Robinhood Chain transaction fee" + body: | + Robinhood Chain native transfer fee is {{p50:robinhood}} (p50, 24h). Robinhood's Arbitrum Orbit rollup posts a very low base fee (~0.05 gwei) and zero priority rewards, the typical Orbit pattern where the centralized sequencer needs no tip market; ETH is the gas asset and the L1 blob-posting cost is excluded from this figure like every other L2 on this page. Computed via `eth_feeHistory` on the official sequencer RPC times live ETH price. - slug: base h2: "Base transaction fee" body: | @@ -264,6 +268,18 @@ providers: mean: avg_over_time(tx_fee_native_transfer_usd{chain="arbitrum",tier="std"}[24h]) success: avg_over_time(tx_fee_health{chain="arbitrum"}[24h]) series: tx_fee_native_transfer_usd{chain="arbitrum",tier="std"} + - slug: robinhood + name: Robinhood Chain + layer: l2 + tag: "Arbitrum Orbit rollup Robinhood, 21000 gas" + formula: "Median USD cost of a native ETH transfer on Robinhood Chain over 24h. L2 execution cost only, L1 data posting fee excluded for now." + queries: + p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain="robinhood",tier="std"}[24h]) + p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain="robinhood",tier="fast"}[24h]) + p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain="robinhood",tier="fast"}[24h]) + mean: avg_over_time(tx_fee_native_transfer_usd{chain="robinhood",tier="std"}[24h]) + success: avg_over_time(tx_fee_health{chain="robinhood"}[24h]) + series: tx_fee_native_transfer_usd{chain="robinhood",tier="std"} - slug: base name: Base layer: l2 diff --git a/harnesses/chain-kpis/cmd/script/registry.go b/harnesses/chain-kpis/cmd/script/registry.go index eb084453..b68a25eb 100644 --- a/harnesses/chain-kpis/cmd/script/registry.go +++ b/harnesses/chain-kpis/cmd/script/registry.go @@ -52,6 +52,7 @@ var Registry = []Chain{ {Slug: "arbitrum", DefiLlama: "Arbitrum", Mobula: "Arbitrum", NativeSymbol: "ETH"}, {Slug: "optimism", DefiLlama: "Optimism", Mobula: "Optimistic", NativeSymbol: "ETH"}, {Slug: "base", DefiLlama: "Base", Mobula: "Base", NativeSymbol: "ETH"}, + {Slug: "robinhood", DefiLlama: "Robinhood Chain", Mobula: "Robinhood Chain", NativeSymbol: "ETH"}, {Slug: "zksync", DefiLlama: "ZKsync Era", Mobula: "ZkSync", NativeSymbol: "ETH"}, {Slug: "linea", DefiLlama: "Linea", Mobula: "Linea", NativeSymbol: "ETH"}, {Slug: "scroll", DefiLlama: "Scroll", Mobula: "Scroll", NativeSymbol: "ETH"}, diff --git a/harnesses/l2-block-time/cmd/script/config.go b/harnesses/l2-block-time/cmd/script/config.go index 98bdb9c4..0879337f 100644 --- a/harnesses/l2-block-time/cmd/script/config.go +++ b/harnesses/l2-block-time/cmd/script/config.go @@ -13,6 +13,12 @@ type L2Chain struct { Slug string Name string URL string + // Poll switches the chain to HTTP block-number polling instead of a + // WebSocket newHeads subscription, for sequencers that expose no + // public WSS (Robinhood Chain). Block time is then derived from + // block-number deltas over each poll window; per-block jitter is + // not captured and the methodology discloses it. + Poll bool } // l2Chains is the source of truth for which Layer-2s appear in the @@ -22,6 +28,12 @@ type L2Chain struct { // rebuild — useful if a public endpoint goes flaky. func l2Chains() []L2Chain { return []L2Chain{ + { + Slug: "robinhood", + Name: "Robinhood Chain", + URL: envDefault("RPC_HTTP_ROBINHOOD", "https://rpc.mainnet.chain.robinhood.com"), + Poll: true, + }, { Slug: "arbitrum", Name: "Arbitrum One", diff --git a/harnesses/l2-block-time/cmd/script/sequencer_poll.go b/harnesses/l2-block-time/cmd/script/sequencer_poll.go new file mode 100644 index 00000000..30a03d10 --- /dev/null +++ b/harnesses/l2-block-time/cmd/script/sequencer_poll.go @@ -0,0 +1,106 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// HTTP polling fallback for sequencers with no public WebSocket +// (Robinhood Chain). Every pollInterval we read eth_blockNumber; a +// window that advanced by N blocks contributes N histogram samples of +// (elapsed / N) ms. This yields an accurate average block time while +// keeping the "one sample per block" weighting of the WS path; what it +// cannot capture is per-block jitter, which the bench methodology +// discloses for polled chains. +const ( + pollInterval = 2 * time.Second + pollTimeout = 10 * time.Second + maxWindowBlend = 600 // safety cap on samples per window (reconnect gaps) +) + +func runPollChain(ch L2Chain) error { + client := &http.Client{Timeout: pollTimeout} + var lastNum int64 + var lastAt time.Time + fails := 0 + + t := time.NewTicker(pollInterval) + defer t.Stop() + fmt.Printf("[%s] HTTP polling mode, interval=%s\n", ch.Slug, pollInterval) + + for now := range t.C { + num, err := fetchBlockNumber(client, ch.URL) + if err != nil || num == 0 { + fails++ + if fails >= 5 { + blockTimeHealth.WithLabelValues(ch.Slug).Set(0) + } + if fails == 5 || fails%150 == 0 { + fmt.Printf("[%s] poll error x%d: %v\n", ch.Slug, fails, err) + } + continue + } + fails = 0 + blockTimeHealth.WithLabelValues(ch.Slug).Set(1) + + if lastNum > 0 && num > lastNum && !lastAt.IsZero() { + delta := num - lastNum + elapsedMs := float64(now.Sub(lastAt).Milliseconds()) + perBlock := elapsedMs / float64(delta) + if perBlock > 0 && perBlock < maxSampleMs { + blockTimeGauge.WithLabelValues(ch.Slug).Set(perBlock) + n := int(delta) + if n > maxWindowBlend { + n = maxWindowBlend + } + for i := 0; i < n; i++ { + blockTimeHistogram.WithLabelValues(ch.Slug).Observe(perBlock) + } + blockTimeSampleCtr.WithLabelValues(ch.Slug).Add(float64(n)) + } + } + lastNum = num + lastAt = now + } + return nil +} + +func fetchBlockNumber(client *http.Client, url string) (int64, error) { + body := []byte(fmt.Sprintf( + `{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":%d}`, + time.Now().UnixNano(), + )) + req, err := http.NewRequest("POST", url, bytes.NewReader(body)) + if err != nil { + return 0, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "OpenChainBench/1.0 (+https://openchainbench.com)") + resp, err := client.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<16)) + if err != nil { + return 0, err + } + if resp.StatusCode != 200 { + return 0, fmt.Errorf("status %d", resp.StatusCode) + } + var env struct { + Result string `json:"result"` + } + if err := json.Unmarshal(raw, &env); err != nil { + return 0, err + } + n := parseHexInt64(env.Result) + if n == 0 { + return 0, fmt.Errorf("empty block number") + } + return n, nil +} diff --git a/harnesses/l2-block-time/cmd/script/sequencer_ws.go b/harnesses/l2-block-time/cmd/script/sequencer_ws.go index 26d59f69..e9327aaf 100644 --- a/harnesses/l2-block-time/cmd/script/sequencer_ws.go +++ b/harnesses/l2-block-time/cmd/script/sequencer_ws.go @@ -75,7 +75,12 @@ func StartSequencerWS() { go func() { backoff := minBackoff for { - err := runChain(ch) + var err error + if ch.Poll { + err = runPollChain(ch) + } else { + err = runChain(ch) + } blockTimeHealth.WithLabelValues(ch.Slug).Set(0) blockTimeReconnectCtr.WithLabelValues(ch.Slug).Inc() if err != nil { diff --git a/harnesses/transaction-fee/cmd/script/config.go b/harnesses/transaction-fee/cmd/script/config.go index 24286094..15a20bb0 100644 --- a/harnesses/transaction-fee/cmd/script/config.go +++ b/harnesses/transaction-fee/cmd/script/config.go @@ -136,6 +136,15 @@ func loadConfig() *Config { HasPriorityMarket: true, Layer: "l2", }, + { + Slug: "robinhood", + Name: "Robinhood Chain", + Kind: KindEVM, + RPCURL: getenvDefault("RPC_ROBINHOOD", "https://rpc.mainnet.chain.robinhood.com"), + MobulaSlug: "ethereum", + HasPriorityMarket: true, + Layer: "l2", + }, { Slug: "base", Name: "Base", diff --git a/src/lib/chains.ts b/src/lib/chains.ts index 29fd4bff..48da5cd7 100644 --- a/src/lib/chains.ts +++ b/src/lib/chains.ts @@ -155,6 +155,14 @@ export const CHAINS: ChainEntry[] = [ description: "OP Stack reference rollup, 2 s sequencer cadence, EIP-4844 blob calldata settlement on Ethereum.", }, + { + slug: "robinhood", + label: "Robinhood Chain", + category: "L2", + nativeSymbol: "ETH", + description: + "Robinhood's Arbitrum Orbit rollup for tokenized stocks, ~100 ms blocks, ETH gas, Ethereum blob settlement, sequencer operated by Robinhood with 10% of chain fees shared to ArbitrumDAO.", + }, { slug: "base", label: "Base", From 987b0c3af3af204f1c8d3bddc4d99aaf1967ec4f Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Mon, 13 Jul 2026 21:30:52 +0200 Subject: [PATCH 02/89] ship solana-rpc to prod: bench 075, harness solana probe path, bump lockstep cache keys --- src/lib/removed-benches.ts | 1 - src/lib/spec.ts | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/removed-benches.ts b/src/lib/removed-benches.ts index d7823ee9..24cda458 100644 --- a/src/lib/removed-benches.ts +++ b/src/lib/removed-benches.ts @@ -36,6 +36,5 @@ export const REMOVED_BENCH_SLUGS = new Set([ "indexing-freshness", "rpc-keyed-latency", "explorer-chain-coverage", - "solana-rpc", "portfolio-chain-coverage", ]); diff --git a/src/lib/spec.ts b/src/lib/spec.ts index 7f9ea805..c05f0512 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -273,7 +273,7 @@ const loadBenchmarkUnfilteredCached = unstable_cache( // buckets (gap rendering fix). Cached v26 entries hold the old // hole-compressed arrays whose indices no longer map onto the nominal // step grid the chart back-computes timestamps from. - ["bench-unfiltered-v27", process.env.VERCEL_ENV === "production" ? "prod" : "all"], + ["bench-unfiltered-v28", process.env.VERCEL_ENV === "production" ? "prod" : "all"], { revalidate: 300, tags: ["benchmarks"] }, ); @@ -428,7 +428,7 @@ const loadAllBenchmarksCached = unstable_cache( // gated catalog to /products for 30+ min after the deploy). // v29: bumped with bench-unfiltered-v26 (monad-rpc + megaeth-rpc ship). // v30: bumped with bench-unfiltered-v27 (dense series with nulls). - ["all-benchmarks-v30", process.env.VERCEL_ENV === "production" ? "prod" : "all"], + ["all-benchmarks-v31", process.env.VERCEL_ENV === "production" ? "prod" : "all"], { revalidate: 300, tags: ["benchmarks"] }, ); export const loadAllBenchmarks = cache(loadAllBenchmarksCached); @@ -507,7 +507,7 @@ const loadBenchmarkFiltered = unstable_cache( // v16: bumped with the bench 074 ship (lockstep rule, see all-benchmarks-v28). // v17: bumped with the monad-rpc + megaeth-rpc ship (lockstep rule). // v18: bumped with bench-unfiltered-v27 (dense series with nulls). - ["bench-filters-v18", process.env.VERCEL_ENV === "production" ? "prod" : "all"], + ["bench-filters-v19", process.env.VERCEL_ENV === "production" ? "prod" : "all"], { revalidate: 300, tags: ["benchmarks"] } ); From 3991057bb8cfbdedd3e399a69db0e9da9d9ed48c Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:13:25 +0200 Subject: [PATCH 03/89] sync: cherry-pick citable/stat drift fixes to main (#1155) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 3 machine-readable surface bugs surfaced by GEO audit 1. /api/citable/{date} unit scaling. Snapshot route emitted `value: 627` for `aggregator-head-lag` while the live /api/citable emitted `value: 0.63` on the same measurement. The live route already wraps with valueInDeclaredUnit(raw, b.unit) but the snapshot route was left with the raw internal-ms value. Wrap the same way; sub-1 s benches now render in the declared `s` unit on both routes. 2. /api/stat/{slug} silently ignored ?chain= / ?region= / ?kind= / ?venue=. getBenchmark accepted a filters object but the route never parsed the URL. A citer asking /api/stat/rpc-capabilities?chain=ethereum got the cross-chain aggregate. Parse the four dimension params, pass through to the loader; unknown values fall back to unfiltered inside the loader. Also widen getBenchmark options type to include kind and venue — the underlying loadBenchmark already supports both. 3. Draft benches leaked a fake freshness signal. Draft placeholder set lastRunAt to new Date for type safety (Benchmark.lastRunAt is non-null), and both citable routes passed it straight through as asOf. LLM crawlers treating asOf as ground truth would think a draft bench was measured every minute. Null asOf in the JSON when status is draft; the internal type stays intact. * fix: propagate draft-asOf null-out to /api/stat, MCP, JSON-LD dateModified The GEO audit review found the initial fix in commit f6f84b8 only covered the two /api/citable JSON surfaces, but the same "draft benches spoof freshness" class of bug lives on three more machine-readable channels that LLM crawlers also key on: - /api/stat/{slug} asOf field. - MCP tool responses (list_benchmarks, get_benchmark, resource template, and the plain-text "Last sample" line). - JSON-LD dateModified on the bench page, per-chain sub-page, answer page and alternatives page — Google, Bing, Perplexity all consume dateModified as a freshness ranking signal. Centralize the guard as citableAsOf(b) in @/lib/citation, rewire every call site. Draft benches now uniformly omit or null the freshness timestamp; live benches unchanged. On the visible /answers page, the "Data as of..." line and its