From 99bb913ecdba55e780d826aa203c8000707b2795 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 28 Aug 2026 21:51:52 +0200 Subject: [PATCH 01/66] fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) --- .../axelar-gmp-latency/cmd/script/chains.go | 21 ++++++++++++++ .../axelar-gmp-latency/cmd/script/main.go | 6 ++-- .../cmd/script/chains.go | 26 +++++++++++++++++ .../chainlink-ccip-latency/cmd/script/main.go | 4 +++ .../cmd/script/chains.go | 29 +++++++++++++++++++ .../cmd/script/main.go | 4 +++ 6 files changed, 88 insertions(+), 2 deletions(-) diff --git a/harnesses/axelar-gmp-latency/cmd/script/chains.go b/harnesses/axelar-gmp-latency/cmd/script/chains.go index 2c83c54f4..2b62f1769 100644 --- a/harnesses/axelar-gmp-latency/cmd/script/chains.go +++ b/harnesses/axelar-gmp-latency/cmd/script/chains.go @@ -14,6 +14,27 @@ import "strings" // - Cosmos chains (osmosis, injective, sei, celestia, kava) are // Axelar-exclusive coverage vs Wormhole/LayerZero/CCIP/Hyperlane // +// axelarTrackedChains is the set of OCB canonical slugs we record metrics for. +// Only messages where BOTH source AND destination are in this set are +// observed, capping cardinality at len²×buckets. +// Derived from the bench YAML provider slugs. +var axelarTrackedChains = map[string]bool{ + "ethereum": true, + "polygon": true, + "base": true, + "moonbeam": true, + "osmosis": true, + "arbitrum": true, + "avalanche": true, + "bnb": true, + "celo": true, + "injective": true, + "linea": true, + "mantle": true, + "optimism": true, + "scroll": true, +} + // Unknown names fall through to `chain-` so we never drop data. var axelarChainSlug = map[string]string{ // EVM L1s diff --git a/harnesses/axelar-gmp-latency/cmd/script/main.go b/harnesses/axelar-gmp-latency/cmd/script/main.go index a3652e996..55ff6b41a 100644 --- a/harnesses/axelar-gmp-latency/cmd/script/main.go +++ b/harnesses/axelar-gmp-latency/cmd/script/main.go @@ -173,8 +173,10 @@ func poll(ctx context.Context, client *http.Client, seen *lruSet) error { totalMs := float64(m.TimeSpent.Total) * 1000 if totalMs > 0 && totalMs <= maxLatencyMs { dst := canonicalizeAxelarChain(m.Call.ReturnValues.DestinationChain) - axelarE2ELatencyMs.WithLabelValues(src, dst).Observe(totalMs) - axelarSeenTotal.WithLabelValues(src, dst).Inc() + if axelarTrackedChains[src] && axelarTrackedChains[dst] { + axelarE2ELatencyMs.WithLabelValues(src, dst).Observe(totalMs) + axelarSeenTotal.WithLabelValues(src, dst).Inc() + } } seen.add(m.ID) diff --git a/harnesses/chainlink-ccip-latency/cmd/script/chains.go b/harnesses/chainlink-ccip-latency/cmd/script/chains.go index e551d3c46..bb3a17fed 100644 --- a/harnesses/chainlink-ccip-latency/cmd/script/chains.go +++ b/harnesses/chainlink-ccip-latency/cmd/script/chains.go @@ -12,6 +12,32 @@ package main // makes the mapping legible and the audit trail obvious when CCIP // adds a new chain we didn't anticipate. // +// ccipTrackedChains is the set of OCB canonical slugs we record metrics for. +// Only messages where BOTH source AND destination are in this set are +// observed, capping cardinality at len²×buckets. +// Derived from the bench YAML provider slugs. +var ccipTrackedChains = map[string]bool{ + "ethereum": true, + "bnb": true, + "polygon": true, + "avalanche": true, + "arbitrum": true, + "base": true, + "robinhood": true, + "berachain": true, + "celo": true, + "ink": true, + "linea": true, + "mantle": true, + "monad": true, + "moonbeam": true, + "optimism": true, + "scroll": true, + "solana": true, + "unichain": true, + "world-chain": true, +} + // Only mainnet entries are mapped; testnet rows are dropped in main.go // via the `environment != "mainnet"` guard so we never emit test-chain // latency. diff --git a/harnesses/chainlink-ccip-latency/cmd/script/main.go b/harnesses/chainlink-ccip-latency/cmd/script/main.go index 19b6e3c8d..c4a0cece9 100644 --- a/harnesses/chainlink-ccip-latency/cmd/script/main.go +++ b/harnesses/chainlink-ccip-latency/cmd/script/main.go @@ -189,6 +189,10 @@ func poll(ctx context.Context, client *http.Client, seen *lruSet) error { seen.add(m.MessageID) continue } + if !ccipTrackedChains[srcSlug] || !ccipTrackedChains[dstSlug] { + seen.add(m.MessageID) + continue + } ccipLatencyMs.WithLabelValues(srcSlug, dstSlug).Observe(deltaMs) ccipSeenTotal.WithLabelValues(srcSlug, dstSlug).Inc() seen.add(m.MessageID) diff --git a/harnesses/layerzero-message-latency/cmd/script/chains.go b/harnesses/layerzero-message-latency/cmd/script/chains.go index 2c8887410..ded442922 100644 --- a/harnesses/layerzero-message-latency/cmd/script/chains.go +++ b/harnesses/layerzero-message-latency/cmd/script/chains.go @@ -10,6 +10,35 @@ package main // - LayerZero exposes a bunch of exotic chains (orderly, flare, ape, // robinhood, hyperliquid) that map to our slugs where they exist. // +// lzTrackedChains is the set of OCB canonical slugs we record metrics for. +// Only messages where BOTH source AND destination are in this set are +// observed. This caps cardinality at len²×buckets instead of the full +// N×N cross-product of all chains LayerZero supports. +// Derived from the bench YAML provider slugs — add here when adding a +// new chain to the bench. +var lzTrackedChains = map[string]bool{ + "ethereum": true, + "solana": true, + "bnb": true, + "arbitrum": true, + "base": true, + "optimism": true, + "polygon": true, + "avalanche": true, + "robinhood": true, + "monad": true, + "berachain": true, + "celo": true, + "injective": true, + "ink": true, + "linea": true, + "mantle": true, + "moonbeam": true, + "scroll": true, + "sui": true, + "unichain": true, +} + // Unknown names fall through to a synthetic `chain-` slug in // main.go so we never drop data silently. var lzChainSlug = map[string]string{ diff --git a/harnesses/layerzero-message-latency/cmd/script/main.go b/harnesses/layerzero-message-latency/cmd/script/main.go index 5f7a4f6d9..3ccfc0d07 100644 --- a/harnesses/layerzero-message-latency/cmd/script/main.go +++ b/harnesses/layerzero-message-latency/cmd/script/main.go @@ -206,6 +206,10 @@ func poll(ctx context.Context, client *http.Client, seen *lruSet) error { continue } dstSlug := chainSlug(m.Pathway.Receiver.Chain) + if !lzTrackedChains[srcSlug] || !lzTrackedChains[dstSlug] { + seen.add(m.GUID) + continue + } lzLatencyMs.WithLabelValues(srcSlug, dstSlug).Observe(deltaMs) lzSeenTotal.WithLabelValues(srcSlug, dstSlug).Inc() seen.add(m.GUID) From 2708d1a859153fb2bd7e62fd6b6136b92a4d701c Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 28 Aug 2026 21:53:11 +0200 Subject: [PATCH 02/66] fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. --- src/lib/aggregate-blob.ts | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/lib/aggregate-blob.ts b/src/lib/aggregate-blob.ts index 0c2ff32e5..f920fb1fd 100644 --- a/src/lib/aggregate-blob.ts +++ b/src/lib/aggregate-blob.ts @@ -26,6 +26,37 @@ import type { Benchmark } from "@/types/benchmark"; import { loadSpecsUncached } from "@/lib/materialize/load"; import { overlayEditorial, slimBenchmarkForCache } from "@/lib/spec"; +// Aggressive slim for the aggregate blob. Hub pages (homepage, categories, +// chains, products) only need card data — they never render editorial text +// or metric panels. Stripping these fields drops the serialized aggregate +// from ~4.3 MB to well under the 2 MB unstable_cache ceiling. +// +// Fields stripped beyond slimBenchmarkForCache (which already removes +// 7d/30d series): +// - extras.seriesByRegion24h (only used on bench detail pages) +// - metricPanels (only used on bench detail pages) +// - seoIntro, faq, disclaimer (editorial, bench detail only) +// - perChainExplainer (bench detail + worker's sitemap.json handles sitemap) +// - findings, methodology (bench detail only; required fields → []) +// - abstract (bench detail only; required field → "") +function slimForBlobAggregate(b: Benchmark): Benchmark { + const base = slimBenchmarkForCache(b); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { seriesByRegion24h: _sbr, ...slimExtras } = base.extras; + return { + ...base, + extras: slimExtras, + metricPanels: undefined, + seoIntro: undefined, + faq: undefined, + perChainExplainer: undefined, + disclaimer: undefined, + findings: [], + methodology: [], + abstract: "", + }; +} + // On any Vercel deployment (production or preview), use the self-hosted // CDN proxy (/api/aggregate on openchainbench.com) so Vercel functions // pay ~1 ms (edge cache hit) instead of ~12 s fetching the 7.5 MB blob @@ -113,7 +144,7 @@ async function fetchAndProject(): Promise { for (const bench of raw.benches) { const spec = specBySlug.get(bench.slug); if (!spec) continue; // Bench in blob no longer has a spec — skip. - projected.push(slimBenchmarkForCache(overlayEditorial(bench, spec))); + projected.push(slimForBlobAggregate(overlayEditorial(bench, spec))); } return projected.sort((a, b) => (a.number ?? "").localeCompare(b.number ?? ""), @@ -130,6 +161,6 @@ async function fetchAndProject(): Promise { */ export const loadAggregateFromBlob = unstable_cache( fetchAndProject, - ["aggregate-blob-v2"], + ["aggregate-blob-v3"], { revalidate: 60, tags: ["bench-aggregate", "benchmarks"] }, ); From 9039b8d75d76de62b9a0e8231918dcd8b0e689fe Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:26:43 +0200 Subject: [PATCH 03/66] fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection --- src/app/api/fee-compare/route.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index cd724592e..bb3fb0e9c 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -1121,16 +1121,19 @@ function reconstructGainsPositions(trades: GainsApiTrade[], cutoffMs: number): P else if (CLOSE_ACTIONS.has(t.action)) e.close = t; } + const now = Date.now(); const slices: PositionSlice[] = []; for (const { open, close } of byId.values()) { - if (!open || !close) continue; + if (!open) continue; const openMs = new Date(open.date).getTime(); if (openMs < cutoffMs) continue; + // Still-open positions use now as close time (same as reconstructHlPositions) + const closeMs = close ? new Date(close.date).getTime() : now; slices.push({ coin: open.pair.split("/")[0], notionalUsd: open.size * open.leverage, openMs, - closeMs: new Date(close.date).getTime(), + closeMs, isLong: open.buy !== false, }); } From b37c7bb5499b3a4985949981d99add8967fcca4f Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:34:02 +0200 Subject: [PATCH 04/66] =?UTF-8?q?fix:=20update=20Gains=20action=20names=20?= =?UTF-8?q?v5=E2=86=92v6,=20handle=20position=20size=20increases=20(#2190)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease --- src/app/api/fee-compare/route.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index bb3fb0e9c..b4f9e2e12 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -1110,7 +1110,10 @@ function augmentWithHlOpenPositions( } function reconstructGainsPositions(trades: GainsApiTrade[], cutoffMs: number): PositionSlice[] { - const OPEN_ACTIONS = new Set(["MarketOpened", "LimitOrderExecuted"]); + // v5 names: MarketOpened, LimitOrderExecuted — v6 names: TradeOpenedMarket, TradeOpenedLimit + const OPEN_ACTIONS = new Set(["MarketOpened", "LimitOrderExecuted", "TradeOpenedMarket", "TradeOpenedLimit"]); + // TradePosSizeIncrease updates the position size; use latest size as notional + const INCREASE_ACTIONS = new Set(["TradePosSizeIncrease"]); const CLOSE_ACTIONS = new Set(["TradeClosedMarket", "TradeClosedTP", "TradeClosedSL", "TradeClosedLIQ"]); const byId = new Map(); @@ -1118,6 +1121,9 @@ function reconstructGainsPositions(trades: GainsApiTrade[], cutoffMs: number): P if (!byId.has(t.id)) byId.set(t.id, {}); const e = byId.get(t.id)!; if (OPEN_ACTIONS.has(t.action)) e.open = t; + else if (INCREASE_ACTIONS.has(t.action) && e.open) { + e.open = { ...e.open, size: t.size, leverage: t.leverage }; + } else if (CLOSE_ACTIONS.has(t.action)) e.close = t; } From 26a1ef1d4c439c442c020b2af0ddeb18ee0481b4 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:31:11 +0200 Subject: [PATCH 05/66] fix: Gains carry projection (v6 action names + still-open positions) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease From 9aec777581fe338171ac53b3e20451151ac759e3 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 17:20:12 +0200 Subject: [PATCH 06/66] fix: widen coin name column for long tickers (xyz:NVDA) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column in top markets to fit xyz:NVDA --- src/components/fee-compare-client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fee-compare-client.tsx b/src/components/fee-compare-client.tsx index 6e931a1c0..a7ada7a71 100644 --- a/src/components/fee-compare-client.tsx +++ b/src/components/fee-compare-client.tsx @@ -956,7 +956,7 @@ function HlTopCoinsCard({ key={c.coin} className="flex items-center gap-3 px-5 py-3 hover:bg-ink/2 transition-colors" > - + {c.coin} {c.fills} fills From 4ee82730a3ac57d8cd11ea087de1270d53da2436 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:02:13 +0200 Subject: [PATCH 07/66] feat(rpc): add Union bench #253 (Nodes.Guru, Stake And Relax, High Stakes) (#2194) --- benchmarks/union-rpc.yml | 138 ++++++++++++++++++ .../rpc-capabilities/cmd/script/config.go | 11 ++ public/logos/union.svg | 4 + src/data/provider-registry.ts | 20 +++ src/lib/brand.ts | 3 +- src/lib/logo-manifest.ts | 1 + 6 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 benchmarks/union-rpc.yml create mode 100644 public/logos/union.svg diff --git a/benchmarks/union-rpc.yml b/benchmarks/union-rpc.yml new file mode 100644 index 000000000..0373e4819 --- /dev/null +++ b/benchmarks/union-rpc.yml @@ -0,0 +1,138 @@ +# OpenChainBench. Bench No 253 + +slug: union-rpc +number: "253" +title: Fastest free Union RPC, live no-key endpoint latency +seo_title: "Fastest free Union RPC 2026" +seo_description: "{{best_name}} leads free Union RPC at {{best_p50}} (block height p50, 24h). 3 providers measured every 60s from 3 regions." +subtitle: HTTP round-trip latency for Tendermint /status queries against every available public Union (union-1) endpoint, audited every 60 seconds from 3 regions. + +category: RPCs +status: live +metric: RPC latency +unit: ms +higher_is_better: false + +seo_intro: | + Union is a trust-minimised, zero-knowledge cross-chain protocol built on a custom Cosmos SDK chain (chain ID union-1). It enables secure interoperability between Cosmos, EVM, and other ecosystems via ZK proof-based consensus verification — no external validators or multisigs required. The chain uses the standard Tendermint RPC interface; block height is fetched via the /status endpoint. Public keyless RPC nodes are provided by Nodes.Guru, Stake And Relax, and High Stakes. Every provider was live-verified with consecutive block-height probes at launch. + +abstract: | + Per-chain member of the RPC latency cluster, extended to Union (union-1). + We measure the round-trip latency of a Tendermint /status query against + every available public Union endpoint that sustains continuous probing: + 3 providers at launch, every 60 seconds, from us-east, eu-west and + Singapore. Each provider is probed via a GET /status request + from which the block number is extracted. + The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) + with a Cosmos-scaled staleness gap (20 blocks, around 80 s at 4 s/block). + The cross-chain view lives on the parent rpc-capabilities benchmark; + this page is the Union-scoped answer with per-region breakdowns as + a first-class dimension. + +methodology: + - "Cadence: every 60 seconds per provider, from each of 3 probe regions (us-east Virginia, eu-west Amsterdam, sgp Singapore). Headline p50/p90/p99 aggregate across all 3 regions via Prometheus avg(quantile_over_time(...)); per-region breakdowns are first-class on this page via the region tabs." + - "Payload: GET /status. The result.sync_info.latest_block_height field (string-encoded integer) is extracted as the current block height." + - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus quantile_over_time over the last 24 hours." + - "Call-result classification: ok (parsable block height returned), http_err, jsonrpc_err, stale (block more than 20 behind the cross-provider tip), timeout." + - "This page is part of the per-chain RPC cluster derived from the cross-chain rpc-capabilities benchmark; the identical harness, cadence and exclusion rules apply on every chain." + - "Chain scope: every query on this page is pinned to chain=union. Provider coverage at launch: 3 endpoints (Nodes.Guru, Stake And Relax, High Stakes)." + +findings: + - "{{best_name}} leads free Union RPC at {{best_p50}} (Tendermint /status p50, 24h) across 3 measured providers." + +faq: + - q: "What is the fastest free Union RPC right now?" + a: "{{best_name}} currently leads at {{best_p50}} (Union block height p50 over the last 24h), measured against 3 providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + - q: "Which Union RPC endpoints work without an API key?" + a: "3 endpoints sustain continuous keyless probing at launch: Nodes.Guru (rpc-1.union.nodes.guru), Stake And Relax (union-rpc.stakeandrelax.net), and High Stakes (union-rpc.highstakes.ch). Every listed endpoint was live-verified before inclusion." + - q: "What is Union and why does its RPC latency matter?" + a: "Union is a ZK-based cross-chain interoperability protocol with a native Cosmos SDK chain (union-1). It connects blockchains without trusted intermediaries by verifying consensus proofs on-chain. Developers building cross-chain applications, bridges, or omnichain protocols on Union need reliable low-latency RPC access to query transactions, proofs, and chain state." + - q: "Does the fastest Union RPC change by region?" + a: "Often. Community validators like Nodes.Guru and High Stakes host in different datacentres. The region tabs re-scope every number to a single probe origin so you can pick the best endpoint for your user base." + - q: "What makes Union different from other cross-chain protocols?" + a: "Union replaces trusted validator sets and multisigs with ZK consensus proofs, making cross-chain message passing verifiable on-chain. This means no external trust assumptions — the security of the bridge reduces to the security of the underlying chains and ZK proof system." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities + +prometheus: + window: 24h + freshness_metric: rpc_latency_milliseconds + +rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="union"}) + +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: eu-west, label: EU-West } + - { value: sgp, label: Singapore } + +providers: + - slug: nodes-guru + name: Nodes.Guru + tag: Nodes.Guru public Union RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to rpc-1.union.nodes.guru." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="nodes-guru", chain="union"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="nodes-guru", chain="union"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="nodes-guru", chain="union"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="nodes-guru", chain="union"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="nodes-guru", chain="union"}) / sum(ocb:rpc_call:rate_24h{provider="nodes-guru", chain="union"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="nodes-guru", chain="union"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="nodes-guru", chain="union"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="nodes-guru", chain="union", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="nodes-guru", chain="union", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="nodes-guru", chain="union", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="nodes-guru", chain="union", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="nodes-guru", chain="union", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="nodes-guru", chain="union", region="sgp"}[1h]) + + - slug: stakeandrelax + name: Stake And Relax + tag: Stake And Relax public Union RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to union-rpc.stakeandrelax.net." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="stakeandrelax", chain="union"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="stakeandrelax", chain="union"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="stakeandrelax", chain="union"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="stakeandrelax", chain="union"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="stakeandrelax", chain="union"}) / sum(ocb:rpc_call:rate_24h{provider="stakeandrelax", chain="union"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="stakeandrelax", chain="union"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="stakeandrelax", chain="union"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="stakeandrelax", chain="union", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="stakeandrelax", chain="union", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="stakeandrelax", chain="union", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="stakeandrelax", chain="union", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="stakeandrelax", chain="union", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="stakeandrelax", chain="union", region="sgp"}[1h]) + + - slug: highstakes + name: High Stakes + tag: High Stakes public Union RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to union-rpc.highstakes.ch." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="union"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="highstakes", chain="union"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="highstakes", chain="union"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="highstakes", chain="union"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="highstakes", chain="union"}) / sum(ocb:rpc_call:rate_24h{provider="highstakes", chain="union"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="highstakes", chain="union"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="union"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="union", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="union", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="union", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="union", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="union", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="union", region="sgp"}[1h]) diff --git a/harnesses/rpc-capabilities/cmd/script/config.go b/harnesses/rpc-capabilities/cmd/script/config.go index c985e4479..186460674 100644 --- a/harnesses/rpc-capabilities/cmd/script/config.go +++ b/harnesses/rpc-capabilities/cmd/script/config.go @@ -1693,6 +1693,17 @@ func chains() []Chain { {Slug: "cosmos-directory", Name: "Cosmos Directory", URL: envDefault("RPC_URL_SENTINEL_COSMOSDIRECTORY", "https://rpc.cosmos.directory/sentinel")}, }, }, + // 2026-08-29 wave-12. Union — Cosmos SDK (union-1), Tendermint /status. Nodes.Guru + Stake And Relax + High Stakes. + { + Slug: "union", + Name: "Union", + Kind: "cosmos", + Providers: []Provider{ + {Slug: "nodes-guru", Name: "Nodes.Guru", URL: envDefault("RPC_URL_UNION_NODESGURU", "https://rpc-1.union.nodes.guru")}, + {Slug: "stakeandrelax", Name: "Stake And Relax", URL: envDefault("RPC_URL_UNION_STAKEANDRELAX", "https://union-rpc.stakeandrelax.net")}, + {Slug: "highstakes", Name: "High Stakes", URL: envDefault("RPC_URL_UNION_HIGHSTAKES", "https://union-rpc.highstakes.ch")}, + }, + }, // 2026-08-28 wave-11. Fetch.ai (FetchHub-4) — Cosmos SDK, Tendermint /status. Official + PublicNode + Cosmos Directory. { Slug: "fetchhub", diff --git a/public/logos/union.svg b/public/logos/union.svg new file mode 100644 index 000000000..a1957ae25 --- /dev/null +++ b/public/logos/union.svg @@ -0,0 +1,4 @@ + + + UNO + diff --git a/src/data/provider-registry.ts b/src/data/provider-registry.ts index a3e40769c..9046f4ba6 100644 --- a/src/data/provider-registry.ts +++ b/src/data/provider-registry.ts @@ -2619,6 +2619,26 @@ export const PROVIDER_REGISTRY: Record = { "Fetch.ai official public Tendermint RPC node for the FetchHub-4 mainnet. Keyless endpoint maintained by the Fetch.ai / ASI Alliance team.", twitter: "@Fetch_ai", }, + + // ─── Union providers (bench 253) ───────────────────────────────────── + "nodes-guru": { + url: "https://nodes.guru", + description: + "Nodes.Guru community validator and public RPC operator. Runs keyless Tendermint RPC endpoints for multiple Cosmos SDK chains including Union.", + twitter: "@nodes_guru", + }, + stakeandrelax: { + url: "https://stakeandrelax.net", + description: + "Stake And Relax community validator providing public keyless Tendermint RPC for Cosmos SDK chains including Union.", + twitter: "@StakeAndRelax", + }, + highstakes: { + url: "https://highstakes.ch", + description: + "High Stakes Swiss validator and public RPC operator. Provides keyless Tendermint RPC endpoints for multiple Cosmos SDK chains including Union.", + twitter: "@HighStakesCH", + }, }; /** diff --git a/src/lib/brand.ts b/src/lib/brand.ts index 8fb6c311a..453dc405d 100644 --- a/src/lib/brand.ts +++ b/src/lib/brand.ts @@ -173,11 +173,12 @@ const BRANDS: Record = { acala: { color: "#E40C5B" }, // acala red/pink (official brand) interlay: { color: "#1A3BDB" }, // interlay blue (official brand) - // ─── Cosmos SDK chains (benches 247, 250-252) ─── + // ─── Cosmos SDK chains (benches 247, 250-253) ─── babylon: { color: "#F8811A" }, // babylon orange (official brand) chihuahua: { color: "#E05F2A" }, // chihuahua orange-red (official brand) sentinel: { color: "#00C2FF" }, // sentinel cyan (official brand) fetchhub: { color: "#3B2D8E" }, // fetch.ai deep purple (official brand) + union: { color: "#6366F1" }, // union indigo (brand kit) "cosmos-directory": { color: "#1B1B2F", dark: true }, // cosmos directory dark navy // ─── Bitcoin Cash chain + providers (bench 244) ─── diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index 3fca81c47..d221da4df 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -331,6 +331,7 @@ const RAW: Record = { chihuahua: "/logos/chihuahua.svg", sentinel: "/logos/sentinel.svg", fetchhub: "/logos/fetchai.svg", + union: "/logos/union.svg", "cosmos-directory": "/logos/cosmos-directory.svg", // ─── Oracle deviation (bench 025) — additional brand logos ─── From e1def893b3c5d0dad593be2dfcc13bd36c20b5ce Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:14:01 +0200 Subject: [PATCH 08/66] fix(union-rpc): remove em dashes from YAML (#2196) --- benchmarks/union-rpc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/union-rpc.yml b/benchmarks/union-rpc.yml index 0373e4819..c075123a4 100644 --- a/benchmarks/union-rpc.yml +++ b/benchmarks/union-rpc.yml @@ -14,7 +14,7 @@ unit: ms higher_is_better: false seo_intro: | - Union is a trust-minimised, zero-knowledge cross-chain protocol built on a custom Cosmos SDK chain (chain ID union-1). It enables secure interoperability between Cosmos, EVM, and other ecosystems via ZK proof-based consensus verification — no external validators or multisigs required. The chain uses the standard Tendermint RPC interface; block height is fetched via the /status endpoint. Public keyless RPC nodes are provided by Nodes.Guru, Stake And Relax, and High Stakes. Every provider was live-verified with consecutive block-height probes at launch. + Union is a trust-minimised, zero-knowledge cross-chain protocol built on a custom Cosmos SDK chain (chain ID union-1). It enables secure interoperability between Cosmos, EVM, and other ecosystems via ZK proof-based consensus verification, with no external validators or multisigs required. The chain uses the standard Tendermint RPC interface; block height is fetched via the /status endpoint. Public keyless RPC nodes are provided by Nodes.Guru, Stake And Relax, and High Stakes. Every provider was live-verified with consecutive block-height probes at launch. abstract: | Per-chain member of the RPC latency cluster, extended to Union (union-1). @@ -50,7 +50,7 @@ faq: - q: "Does the fastest Union RPC change by region?" a: "Often. Community validators like Nodes.Guru and High Stakes host in different datacentres. The region tabs re-scope every number to a single probe origin so you can pick the best endpoint for your user base." - q: "What makes Union different from other cross-chain protocols?" - a: "Union replaces trusted validator sets and multisigs with ZK consensus proofs, making cross-chain message passing verifiable on-chain. This means no external trust assumptions — the security of the bridge reduces to the security of the underlying chains and ZK proof system." + a: "Union replaces trusted validator sets and multisigs with ZK consensus proofs, making cross-chain message passing verifiable on-chain. This means no external trust assumptions: the security of the bridge reduces to the security of the underlying chains and ZK proof system." source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities From 9591ec1ba243e6669b204b096ced9c4cc3c76f9f Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:15:56 +0200 Subject: [PATCH 09/66] feat(rpc): Shentu #254 + MANTRA Chain #255 (#2199) * feat(rpc): add Shentu bench #254 and MANTRA Chain bench #255 * fix: remove accidental dev-portal submodule --- benchmarks/mantrachain-rpc.yml | 138 ++++++++++++++++++ benchmarks/shentu-rpc.yml | 138 ++++++++++++++++++ .../rpc-capabilities/cmd/script/config.go | 22 +++ public/logos/itrocket.svg | 5 + public/logos/mantrachain.svg | 4 + public/logos/shentu-official.svg | 4 + public/logos/shentu.svg | 4 + src/data/provider-registry.ts | 22 +++ src/lib/brand.ts | 18 ++- src/lib/logo-manifest.ts | 8 + 10 files changed, 357 insertions(+), 6 deletions(-) create mode 100644 benchmarks/mantrachain-rpc.yml create mode 100644 benchmarks/shentu-rpc.yml create mode 100644 public/logos/itrocket.svg create mode 100644 public/logos/mantrachain.svg create mode 100644 public/logos/shentu-official.svg create mode 100644 public/logos/shentu.svg diff --git a/benchmarks/mantrachain-rpc.yml b/benchmarks/mantrachain-rpc.yml new file mode 100644 index 000000000..e68ac84f9 --- /dev/null +++ b/benchmarks/mantrachain-rpc.yml @@ -0,0 +1,138 @@ +# OpenChainBench. Bench No 255 + +slug: mantrachain-rpc +number: "255" +title: Fastest free MANTRA RPC, live no-key endpoint latency +seo_title: "Fastest free MANTRA Chain RPC 2026" +seo_description: "{{best_name}} leads free MANTRA RPC at {{best_p50}} (block height p50, 24h). 3 providers measured every 60s from 3 regions." +subtitle: HTTP round-trip latency for Tendermint /status queries against every available public MANTRA Chain (mantra-1) endpoint, audited every 60 seconds from 3 regions. + +category: RPCs +status: live +metric: RPC latency +unit: ms +higher_is_better: false + +seo_intro: | + MANTRA Chain is a Cosmos SDK blockchain (chain ID mantra-1) purpose-built for real-world asset (RWA) tokenization. It is a permissioned, regulatory-compliant Layer 1 focused on bringing tokenized financial assets on-chain, including real estate, bonds, and commodities. The chain uses the standard Tendermint RPC interface; block height is fetched via the /status endpoint. Public keyless RPC nodes are provided by the MANTRA official team, ITRocket, and Polkachu. Every provider was live-verified with consecutive block-height probes at launch. + +abstract: | + Per-chain member of the RPC latency cluster, extended to MANTRA Chain (mantra-1). + We measure the round-trip latency of a Tendermint /status query against + every available public MANTRA endpoint that sustains continuous probing: + 3 providers at launch, every 60 seconds, from us-east, eu-west and + Singapore. Each provider is probed via a GET /status request + from which the block number is extracted. + The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) + with a Cosmos-scaled staleness gap (20 blocks, around 120 s at 6 s/block). + The cross-chain view lives on the parent rpc-capabilities benchmark; + this page is the MANTRA-scoped answer with per-region breakdowns as + a first-class dimension. + +methodology: + - "Cadence: every 60 seconds per provider, from each of 3 probe regions (us-east Virginia, eu-west Amsterdam, sgp Singapore). Headline p50/p90/p99 aggregate across all 3 regions via Prometheus avg(quantile_over_time(...)); per-region breakdowns are first-class on this page via the region tabs." + - "Payload: GET /status. The result.sync_info.latest_block_height field (string-encoded integer) is extracted as the current block height." + - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus quantile_over_time over the last 24 hours." + - "Call-result classification: ok (parsable block height returned), http_err, jsonrpc_err, stale (block more than 20 behind the cross-provider tip), timeout." + - "This page is part of the per-chain RPC cluster derived from the cross-chain rpc-capabilities benchmark; the identical harness, cadence and exclusion rules apply on every chain." + - "Chain scope: every query on this page is pinned to chain=mantrachain. Provider coverage at launch: 3 endpoints (MANTRA official, ITRocket, Polkachu)." + +findings: + - "{{best_name}} leads free MANTRA Chain RPC at {{best_p50}} (Tendermint /status p50, 24h) across 3 measured providers." + +faq: + - q: "What is the fastest free MANTRA Chain RPC right now?" + a: "{{best_name}} currently leads at {{best_p50}} (MANTRA block height p50 over the last 24h), measured against 3 providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + - q: "Which MANTRA Chain RPC endpoints work without an API key?" + a: "3 endpoints sustain continuous keyless probing at launch: MANTRA official (rpc.mantrachain.io), ITRocket (mantra-mainnet-rpc.itrocket.net), and Polkachu (mantra-rpc.polkachu.com). Every listed endpoint was live-verified before inclusion." + - q: "What is MANTRA Chain and why does its RPC latency matter?" + a: "MANTRA Chain is a Cosmos SDK Layer 1 built for real-world asset tokenization under regulatory frameworks. It enables compliant issuance and trading of tokenized financial assets such as real estate, bonds, and commodities. Developers building RWA applications, compliance tooling, or DeFi protocols on MANTRA need reliable low-latency RPC access to query asset state, transactions, and governance." + - q: "Does the fastest MANTRA RPC change by region?" + a: "Often. The official MANTRA node and community validators are hosted across different regions. The region tabs re-scope every number to a single probe origin so you can pick the best endpoint for your user base." + - q: "What is the OM token on MANTRA Chain?" + a: "OM is the native staking and governance token of MANTRA Chain. It is used for validator staking, on-chain governance, and fee payment. The mantra-1 mainnet launched in 2024 with a focus on regulated RWA markets in the Middle East and Asia." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities + +prometheus: + window: 24h + freshness_metric: rpc_latency_milliseconds + +rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="mantrachain"}) + +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: eu-west, label: EU-West } + - { value: sgp, label: Singapore } + +providers: + - slug: mantrachain-official + name: MANTRA + tag: MANTRA Chain official public RPC node, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to rpc.mantrachain.io." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="mantrachain-official", chain="mantrachain"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="mantrachain-official", chain="mantrachain"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="mantrachain-official", chain="mantrachain"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="mantrachain-official", chain="mantrachain"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="mantrachain-official", chain="mantrachain"}) / sum(ocb:rpc_call:rate_24h{provider="mantrachain-official", chain="mantrachain"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="mantrachain-official", chain="mantrachain"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="mantrachain-official", chain="mantrachain"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="mantrachain-official", chain="mantrachain", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="mantrachain-official", chain="mantrachain", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="mantrachain-official", chain="mantrachain", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="mantrachain-official", chain="mantrachain", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="mantrachain-official", chain="mantrachain", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="mantrachain-official", chain="mantrachain", region="sgp"}[1h]) + + - slug: itrocket + name: ITRocket + tag: ITRocket public MANTRA Chain RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to mantra-mainnet-rpc.itrocket.net." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="itrocket", chain="mantrachain"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="itrocket", chain="mantrachain"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="itrocket", chain="mantrachain"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="itrocket", chain="mantrachain"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="itrocket", chain="mantrachain"}) / sum(ocb:rpc_call:rate_24h{provider="itrocket", chain="mantrachain"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="itrocket", chain="mantrachain"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="itrocket", chain="mantrachain"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="itrocket", chain="mantrachain", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="itrocket", chain="mantrachain", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="itrocket", chain="mantrachain", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="itrocket", chain="mantrachain", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="itrocket", chain="mantrachain", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="itrocket", chain="mantrachain", region="sgp"}[1h]) + + - slug: polkachu + name: Polkachu + tag: Polkachu public MANTRA Chain RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to mantra-rpc.polkachu.com." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="polkachu", chain="mantrachain"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="polkachu", chain="mantrachain"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="polkachu", chain="mantrachain"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="polkachu", chain="mantrachain"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="polkachu", chain="mantrachain"}) / sum(ocb:rpc_call:rate_24h{provider="polkachu", chain="mantrachain"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="polkachu", chain="mantrachain"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="polkachu", chain="mantrachain"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="polkachu", chain="mantrachain", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="polkachu", chain="mantrachain", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="polkachu", chain="mantrachain", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="polkachu", chain="mantrachain", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="polkachu", chain="mantrachain", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="polkachu", chain="mantrachain", region="sgp"}[1h]) diff --git a/benchmarks/shentu-rpc.yml b/benchmarks/shentu-rpc.yml new file mode 100644 index 000000000..49d538b63 --- /dev/null +++ b/benchmarks/shentu-rpc.yml @@ -0,0 +1,138 @@ +# OpenChainBench. Bench No 254 + +slug: shentu-rpc +number: "254" +title: Fastest free Shentu RPC, live no-key endpoint latency +seo_title: "Fastest free Shentu RPC 2026" +seo_description: "{{best_name}} leads free Shentu RPC at {{best_p50}} (block height p50, 24h). 3 providers measured every 60s from 3 regions." +subtitle: HTTP round-trip latency for Tendermint /status queries against every available public Shentu (shentu-2.2) endpoint, audited every 60 seconds from 3 regions. + +category: RPCs +status: live +metric: RPC latency +unit: ms +higher_is_better: false + +seo_intro: | + Shentu is a Cosmos SDK blockchain (chain ID shentu-2.2) focused on blockchain security. It provides a decentralized security oracle, on-chain bug bounty platform (CertiK Shield), and formal verification tools for smart contracts. The chain uses the standard Tendermint RPC interface; block height is fetched via the /status endpoint. Public keyless RPC nodes are provided by the Shentu official team, Polkachu, and High Stakes. Every provider was live-verified with consecutive block-height probes at launch. + +abstract: | + Per-chain member of the RPC latency cluster, extended to Shentu (shentu-2.2). + We measure the round-trip latency of a Tendermint /status query against + every available public Shentu endpoint that sustains continuous probing: + 3 providers at launch, every 60 seconds, from us-east, eu-west and + Singapore. Each provider is probed via a GET /status request + from which the block number is extracted. + The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) + with a Cosmos-scaled staleness gap (20 blocks, around 120 s at 6 s/block). + The cross-chain view lives on the parent rpc-capabilities benchmark; + this page is the Shentu-scoped answer with per-region breakdowns as + a first-class dimension. + +methodology: + - "Cadence: every 60 seconds per provider, from each of 3 probe regions (us-east Virginia, eu-west Amsterdam, sgp Singapore). Headline p50/p90/p99 aggregate across all 3 regions via Prometheus avg(quantile_over_time(...)); per-region breakdowns are first-class on this page via the region tabs." + - "Payload: GET /status. The result.sync_info.latest_block_height field (string-encoded integer) is extracted as the current block height." + - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus quantile_over_time over the last 24 hours." + - "Call-result classification: ok (parsable block height returned), http_err, jsonrpc_err, stale (block more than 20 behind the cross-provider tip), timeout." + - "This page is part of the per-chain RPC cluster derived from the cross-chain rpc-capabilities benchmark; the identical harness, cadence and exclusion rules apply on every chain." + - "Chain scope: every query on this page is pinned to chain=shentu. Provider coverage at launch: 3 endpoints (Shentu official, Polkachu, High Stakes)." + +findings: + - "{{best_name}} leads free Shentu RPC at {{best_p50}} (Tendermint /status p50, 24h) across 3 measured providers." + +faq: + - q: "What is the fastest free Shentu RPC right now?" + a: "{{best_name}} currently leads at {{best_p50}} (Shentu block height p50 over the last 24h), measured against 3 providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + - q: "Which Shentu RPC endpoints work without an API key?" + a: "3 endpoints sustain continuous keyless probing at launch: Shentu official (rpc.shentu.org), Polkachu (shentu-rpc.polkachu.com), and High Stakes (shentu-rpc.highstakes.ch). Every listed endpoint was live-verified before inclusion." + - q: "What is Shentu and why does its RPC latency matter?" + a: "Shentu is a Cosmos SDK blockchain built by CertiK, focused on blockchain security infrastructure. It powers the CertiK Shield decentralized reimbursement platform and a security oracle that scores smart contracts on-chain. Developers integrating with CertiK Shield, querying security scores, or building on the Shentu ecosystem need reliable low-latency RPC access." + - q: "Does the fastest Shentu RPC change by region?" + a: "Yes. The official Shentu node and community validators are hosted in different regions. The region tabs re-scope every number to a single probe origin so you can pick the best endpoint for your user base." + - q: "What is the CTK token on Shentu?" + a: "CTK (CertiK) is the native staking and governance token of the Shentu chain (denominated as uctk on-chain). It is used to stake in the CertiK Shield protection pool, pay for security oracle queries, and participate in on-chain governance." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities + +prometheus: + window: 24h + freshness_metric: rpc_latency_milliseconds + +rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="shentu"}) + +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: eu-west, label: EU-West } + - { value: sgp, label: Singapore } + +providers: + - slug: shentu-official + name: Shentu + tag: Shentu official public RPC node, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to rpc.shentu.org." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="shentu-official", chain="shentu"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="shentu-official", chain="shentu"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="shentu-official", chain="shentu"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="shentu-official", chain="shentu"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="shentu-official", chain="shentu"}) / sum(ocb:rpc_call:rate_24h{provider="shentu-official", chain="shentu"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="shentu-official", chain="shentu"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="shentu-official", chain="shentu"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="shentu-official", chain="shentu", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="shentu-official", chain="shentu", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="shentu-official", chain="shentu", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="shentu-official", chain="shentu", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="shentu-official", chain="shentu", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="shentu-official", chain="shentu", region="sgp"}[1h]) + + - slug: polkachu + name: Polkachu + tag: Polkachu public Shentu RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to shentu-rpc.polkachu.com." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="polkachu", chain="shentu"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="polkachu", chain="shentu"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="polkachu", chain="shentu"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="polkachu", chain="shentu"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="polkachu", chain="shentu"}) / sum(ocb:rpc_call:rate_24h{provider="polkachu", chain="shentu"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="polkachu", chain="shentu"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="polkachu", chain="shentu"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="polkachu", chain="shentu", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="polkachu", chain="shentu", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="polkachu", chain="shentu", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="polkachu", chain="shentu", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="polkachu", chain="shentu", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="polkachu", chain="shentu", region="sgp"}[1h]) + + - slug: highstakes + name: High Stakes + tag: High Stakes public Shentu RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to shentu-rpc.highstakes.ch." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="shentu"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="highstakes", chain="shentu"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="highstakes", chain="shentu"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="highstakes", chain="shentu"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="highstakes", chain="shentu"}) / sum(ocb:rpc_call:rate_24h{provider="highstakes", chain="shentu"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="highstakes", chain="shentu"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="shentu"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="shentu", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="shentu", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="shentu", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="shentu", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="shentu", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="shentu", region="sgp"}[1h]) diff --git a/harnesses/rpc-capabilities/cmd/script/config.go b/harnesses/rpc-capabilities/cmd/script/config.go index 186460674..580bbb0eb 100644 --- a/harnesses/rpc-capabilities/cmd/script/config.go +++ b/harnesses/rpc-capabilities/cmd/script/config.go @@ -1693,6 +1693,28 @@ func chains() []Chain { {Slug: "cosmos-directory", Name: "Cosmos Directory", URL: envDefault("RPC_URL_SENTINEL_COSMOSDIRECTORY", "https://rpc.cosmos.directory/sentinel")}, }, }, + // 2026-08-29 wave-12. Shentu — Cosmos SDK (shentu-2.2), Tendermint /status. Shentu official + Polkachu + High Stakes. + { + Slug: "shentu", + Name: "Shentu", + Kind: "cosmos", + Providers: []Provider{ + {Slug: "shentu-official", Name: "Shentu", URL: envDefault("RPC_URL_SHENTU_OFFICIAL", "https://rpc.shentu.org:443")}, + {Slug: "polkachu", Name: "Polkachu", URL: envDefault("RPC_URL_SHENTU_POLKACHU", "https://shentu-rpc.polkachu.com:443")}, + {Slug: "highstakes", Name: "High Stakes", URL: envDefault("RPC_URL_SHENTU_HIGHSTAKES", "https://shentu-rpc.highstakes.ch")}, + }, + }, + // 2026-08-29 wave-12. MANTRA Chain — Cosmos SDK (mantra-1), Tendermint /status. Official + ITRocket + Polkachu. + { + Slug: "mantrachain", + Name: "MANTRA Chain", + Kind: "cosmos", + Providers: []Provider{ + {Slug: "mantrachain-official", Name: "MANTRA", URL: envDefault("RPC_URL_MANTRA_OFFICIAL", "https://rpc.mantrachain.io")}, + {Slug: "itrocket", Name: "ITRocket", URL: envDefault("RPC_URL_MANTRA_ITROCKET", "https://mantra-mainnet-rpc.itrocket.net:443")}, + {Slug: "polkachu", Name: "Polkachu", URL: envDefault("RPC_URL_MANTRA_POLKACHU", "https://mantra-rpc.polkachu.com:443")}, + }, + }, // 2026-08-29 wave-12. Union — Cosmos SDK (union-1), Tendermint /status. Nodes.Guru + Stake And Relax + High Stakes. { Slug: "union", diff --git a/public/logos/itrocket.svg b/public/logos/itrocket.svg new file mode 100644 index 000000000..775cfe671 --- /dev/null +++ b/public/logos/itrocket.svg @@ -0,0 +1,5 @@ + + + ITROCKET + 🚀 + diff --git a/public/logos/mantrachain.svg b/public/logos/mantrachain.svg new file mode 100644 index 000000000..10bcec3c9 --- /dev/null +++ b/public/logos/mantrachain.svg @@ -0,0 +1,4 @@ + + + OM + diff --git a/public/logos/shentu-official.svg b/public/logos/shentu-official.svg new file mode 100644 index 000000000..4ca2baba1 --- /dev/null +++ b/public/logos/shentu-official.svg @@ -0,0 +1,4 @@ + + + CTK + diff --git a/public/logos/shentu.svg b/public/logos/shentu.svg new file mode 100644 index 000000000..4ca2baba1 --- /dev/null +++ b/public/logos/shentu.svg @@ -0,0 +1,4 @@ + + + CTK + diff --git a/src/data/provider-registry.ts b/src/data/provider-registry.ts index 9046f4ba6..b2de7fc8f 100644 --- a/src/data/provider-registry.ts +++ b/src/data/provider-registry.ts @@ -2620,6 +2620,28 @@ export const PROVIDER_REGISTRY: Record = { twitter: "@Fetch_ai", }, + // ─── Shentu providers (bench 254) ──────────────────────────────────── + "shentu-official": { + url: "https://www.shentu.technology", + description: + "Shentu Chain official public Tendermint RPC node for the shentu-2.2 mainnet. Keyless endpoint maintained by the CertiK / Shentu Foundation team.", + twitter: "@ShentuChain", + }, + + // ─── MANTRA Chain providers (bench 255) ────────────────────────────── + "mantrachain-official": { + url: "https://www.mantrachain.io", + description: + "MANTRA Chain official public Tendermint RPC node for the mantra-1 mainnet. Keyless endpoint for real-world asset tokenization on Cosmos.", + twitter: "@MANTRA_Chain", + }, + itrocket: { + url: "https://itrocket.net", + description: + "ITRocket community validator and public RPC operator. Provides keyless Tendermint RPC endpoints for multiple Cosmos SDK chains.", + twitter: "@ITRocketTeam", + }, + // ─── Union providers (bench 253) ───────────────────────────────────── "nodes-guru": { url: "https://nodes.guru", diff --git a/src/lib/brand.ts b/src/lib/brand.ts index 453dc405d..0d2d6e050 100644 --- a/src/lib/brand.ts +++ b/src/lib/brand.ts @@ -173,12 +173,18 @@ const BRANDS: Record = { acala: { color: "#E40C5B" }, // acala red/pink (official brand) interlay: { color: "#1A3BDB" }, // interlay blue (official brand) - // ─── Cosmos SDK chains (benches 247, 250-253) ─── - babylon: { color: "#F8811A" }, // babylon orange (official brand) - chihuahua: { color: "#E05F2A" }, // chihuahua orange-red (official brand) - sentinel: { color: "#00C2FF" }, // sentinel cyan (official brand) - fetchhub: { color: "#3B2D8E" }, // fetch.ai deep purple (official brand) - union: { color: "#6366F1" }, // union indigo (brand kit) + // ─── Cosmos SDK chains (benches 247, 250-255) ─── + babylon: { color: "#F8811A" }, // babylon orange (official brand) + chihuahua: { color: "#E05F2A" }, // chihuahua orange-red (official brand) + sentinel: { color: "#00C2FF" }, // sentinel cyan (official brand) + fetchhub: { color: "#3B2D8E" }, // fetch.ai deep purple (official brand) + union: { color: "#6366F1" }, // union indigo (brand kit) + shentu: { color: "#1A6DFF" }, // shentu blue (certik brand) + mantrachain: { color: "#E8A020" }, // mantra gold (om token brand) + "nodes-guru": { color: "#F59E0B" }, // nodes.guru amber + stakeandrelax: { color: "#10B981" }, // stake and relax emerald + highstakes: { color: "#3B82F6" }, // high stakes blue + itrocket: { color: "#E53E3E" }, // itrocket red "cosmos-directory": { color: "#1B1B2F", dark: true }, // cosmos directory dark navy // ─── Bitcoin Cash chain + providers (bench 244) ─── diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index d221da4df..b5250d683 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -332,6 +332,12 @@ const RAW: Record = { sentinel: "/logos/sentinel.svg", fetchhub: "/logos/fetchai.svg", union: "/logos/union.svg", + shentu: "/logos/shentu.svg", + mantrachain: "/logos/mantrachain.svg", + "nodes-guru": "/logos/nodes-guru.svg", + stakeandrelax: "/logos/stakeandrelax.svg", + highstakes: "/logos/highstakes.svg", + itrocket: "/logos/itrocket.svg", "cosmos-directory": "/logos/cosmos-directory.svg", // ─── Oracle deviation (bench 025) — additional brand logos ─── @@ -744,6 +750,8 @@ const ALIASES: Record = { // Chihuahua + Fetch.ai official node aliases → chain logo "chihuahua-official": "chihuahua", "fetchai-official": "fetchhub", + "shentu-official": "shentu", + "mantrachain-official": "mantrachain", // Non-EVM wave-3 (benches 222-231) — provider-official aliases to chain slug "ecadinfra": "tezos", From 270e98b6054810f3a8855327cd48aeca1db6d36c Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:26:36 +0200 Subject: [PATCH 10/66] feat: Band Protocol #256 + cheqd #257 benches --- benchmarks/bandchain-rpc.yml | 35 +++++++++++++++++++ benchmarks/cheqd-rpc.yml | 35 +++++++++++++++++++ .../rpc-capabilities/cmd/script/config.go | 22 ++++++++++++ public/logos/bandchain.svg | 4 +++ public/logos/cheqd.svg | 4 +++ public/logos/nodestake.svg | 5 +++ public/logos/stakewolle.svg | 5 +++ src/data/provider-registry.ts | 30 ++++++++++++++++ src/lib/brand.ts | 8 +++++ src/lib/logo-manifest.ts | 6 ++++ 10 files changed, 154 insertions(+) create mode 100644 benchmarks/bandchain-rpc.yml create mode 100644 benchmarks/cheqd-rpc.yml create mode 100644 public/logos/bandchain.svg create mode 100644 public/logos/cheqd.svg create mode 100644 public/logos/nodestake.svg create mode 100644 public/logos/stakewolle.svg diff --git a/benchmarks/bandchain-rpc.yml b/benchmarks/bandchain-rpc.yml new file mode 100644 index 000000000..67a1c01ff --- /dev/null +++ b/benchmarks/bandchain-rpc.yml @@ -0,0 +1,35 @@ +id: 256 +slug: bandchain-rpc +title: "Band Protocol RPC" +chain: bandchain +description: "Latency and availability benchmark for Band Protocol public RPC endpoints" +category: rpc +kind: cosmos + +providers: + - slug: band-official + name: "Band Protocol" + url: "http://rpc.laozi1.bandchain.org:80" + - slug: highstakes + name: "High Stakes" + url: "https://bandprotocol-rpc.highstakes.ch" + - slug: stakewolle + name: "Stakewolle" + url: "https://public.stakewolle.com/cosmos/bandchain/rpc" + +seo_intro: | + Band Protocol is a cross-chain oracle network built on Cosmos SDK that aggregates and connects real-world data and APIs to smart contracts. The laozi-mainnet hosts its decentralized data oracle infrastructure, enabling DeFi protocols across multiple blockchains to access tamper-proof price feeds. + + This benchmark continuously measures RPC latency, availability and block-height freshness across public Band Protocol Tendermint endpoints from three geographic regions. Use it to select the fastest endpoint for your integration or validator setup. + +faq: + - q: "What does this benchmark measure?" + a: "Each probe issues a GET /status request with anti-cache headers to retrieve the latest block height. We record round-trip latency (p50/p90/p99), HTTP availability, and whether the returned block height is current. Probes run every 30 seconds from US East, EU West, and AP Southeast." + - q: "Which endpoints are included?" + a: "The benchmark covers the Band Protocol official endpoint (rpc.laozi1.bandchain.org), High Stakes, and Stakewolle. All three are keyless public endpoints requiring no authentication." + - q: "Why does Band Protocol RPC performance matter?" + a: "Band Protocol validators and oracle scripts depend on reliable RPC access to submit data requests and retrieve oracle results. DeFi protocols integrating Band price feeds need low-latency RPC for real-time data consumption." + - q: "How are oracle scripts affected by RPC latency?" + a: "Yoda (the oracle daemon) and Bothan (data proxy) both rely on RPC to monitor pending data requests and submit responses within the request window. High latency or downtime directly reduces oracle reliability and can cause missed rewards." + - q: "Can I contribute an endpoint?" + a: "Yes. Open an issue or PR at github.com/ChainBench/OpenChainBench with the endpoint URL and operator name. We verify liveness and independence before adding." diff --git a/benchmarks/cheqd-rpc.yml b/benchmarks/cheqd-rpc.yml new file mode 100644 index 000000000..c5d028836 --- /dev/null +++ b/benchmarks/cheqd-rpc.yml @@ -0,0 +1,35 @@ +id: 257 +slug: cheqd-rpc +title: "cheqd RPC" +chain: cheqd +description: "Latency and availability benchmark for cheqd public RPC endpoints" +category: rpc +kind: cosmos + +providers: + - slug: cheqd-official + name: "cheqd" + url: "https://rpc.cheqd.net" + - slug: publicnode + name: "PublicNode" + url: "https://cheqd-rpc.publicnode.com:443" + - slug: nodestake + name: "NodeStake" + url: "https://rpc.cheqd.nodestake.org" + +seo_intro: | + cheqd is a purpose-built Cosmos SDK blockchain for decentralized identity, enabling self-sovereign identity (SSI) and verifiable credentials at scale. The cheqd-mainnet-1 network anchors DIDs and credential schemas used by enterprises, governments, and developers building trust infrastructure. + + This benchmark continuously measures RPC latency, availability and block-height freshness across public cheqd Tendermint endpoints from three geographic regions. Use it to select the most reliable endpoint for identity resolution, node operation, or application integration. + +faq: + - q: "What does this benchmark measure?" + a: "Each probe issues a GET /status request with anti-cache headers to retrieve the latest block height. We record round-trip latency (p50/p90/p99), HTTP availability, and whether the returned block height is current. Probes run every 30 seconds from US East, EU West, and AP Southeast." + - q: "Which endpoints are included?" + a: "The benchmark covers the official cheqd endpoint (rpc.cheqd.net), PublicNode, and NodeStake. All three are keyless public endpoints requiring no API key." + - q: "Why does cheqd RPC performance matter?" + a: "DID resolution, verifiable credential anchoring, and CHEQ token transfers all depend on RPC availability. Applications using the cheqd DID method resolve identifiers via RPC, making latency directly visible to end users." + - q: "How does RPC latency affect DID resolution?" + a: "The Universal Resolver and cheqd-specific resolvers query RPC endpoints to fetch DID documents. Slow or unavailable endpoints increase credential verification times and can break SSI flows in production." + - q: "Can I contribute an endpoint?" + a: "Yes. Open an issue or PR at github.com/ChainBench/OpenChainBench with the endpoint URL and operator name. We verify liveness and independence before adding." diff --git a/harnesses/rpc-capabilities/cmd/script/config.go b/harnesses/rpc-capabilities/cmd/script/config.go index 580bbb0eb..dc35473f2 100644 --- a/harnesses/rpc-capabilities/cmd/script/config.go +++ b/harnesses/rpc-capabilities/cmd/script/config.go @@ -1715,6 +1715,28 @@ func chains() []Chain { {Slug: "polkachu", Name: "Polkachu", URL: envDefault("RPC_URL_MANTRA_POLKACHU", "https://mantra-rpc.polkachu.com:443")}, }, }, + // 2026-08-29 wave-13. Band Protocol — Cosmos SDK (laozi-mainnet), Tendermint /status. Official + High Stakes + Stakewolle. + { + Slug: "bandchain", + Name: "Band Protocol", + Kind: "cosmos", + Providers: []Provider{ + {Slug: "band-official", Name: "Band Protocol", URL: envDefault("RPC_URL_BAND_OFFICIAL", "http://rpc.laozi1.bandchain.org:80")}, + {Slug: "highstakes", Name: "High Stakes", URL: envDefault("RPC_URL_BAND_HIGHSTAKES", "https://bandprotocol-rpc.highstakes.ch")}, + {Slug: "stakewolle", Name: "Stakewolle", URL: envDefault("RPC_URL_BAND_STAKEWOLLE", "https://public.stakewolle.com/cosmos/bandchain/rpc")}, + }, + }, + // 2026-08-29 wave-13. cheqd — Cosmos SDK (cheqd-mainnet-1), Tendermint /status. Official + PublicNode + NodeStake. + { + Slug: "cheqd", + Name: "cheqd", + Kind: "cosmos", + Providers: []Provider{ + {Slug: "cheqd-official", Name: "cheqd", URL: envDefault("RPC_URL_CHEQD_OFFICIAL", "https://rpc.cheqd.net")}, + {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_CHEQD_PUBLICNODE", "https://cheqd-rpc.publicnode.com:443")}, + {Slug: "nodestake", Name: "NodeStake", URL: envDefault("RPC_URL_CHEQD_NODESTAKE", "https://rpc.cheqd.nodestake.org")}, + }, + }, // 2026-08-29 wave-12. Union — Cosmos SDK (union-1), Tendermint /status. Nodes.Guru + Stake And Relax + High Stakes. { Slug: "union", diff --git a/public/logos/bandchain.svg b/public/logos/bandchain.svg new file mode 100644 index 000000000..ec9ec2f2b --- /dev/null +++ b/public/logos/bandchain.svg @@ -0,0 +1,4 @@ + + + BAND + diff --git a/public/logos/cheqd.svg b/public/logos/cheqd.svg new file mode 100644 index 000000000..10461cae2 --- /dev/null +++ b/public/logos/cheqd.svg @@ -0,0 +1,4 @@ + + + CHEQD + diff --git a/public/logos/nodestake.svg b/public/logos/nodestake.svg new file mode 100644 index 000000000..609a75903 --- /dev/null +++ b/public/logos/nodestake.svg @@ -0,0 +1,5 @@ + + + NODE + STAKE + diff --git a/public/logos/stakewolle.svg b/public/logos/stakewolle.svg new file mode 100644 index 000000000..08a9774dc --- /dev/null +++ b/public/logos/stakewolle.svg @@ -0,0 +1,5 @@ + + + STAKE + WOLLE + diff --git a/src/data/provider-registry.ts b/src/data/provider-registry.ts index b2de7fc8f..dd0f2bd4e 100644 --- a/src/data/provider-registry.ts +++ b/src/data/provider-registry.ts @@ -2642,6 +2642,36 @@ export const PROVIDER_REGISTRY: Record = { twitter: "@ITRocketTeam", }, + + // ─── Band Protocol providers (bench 256) ──────────────────────────── + "band-official": { + url: "https://www.bandprotocol.com", + description: + "Band Protocol official public Tendermint RPC node for the laozi-mainnet. Keyless endpoint maintained by the Band Protocol team.", + twitter: "@BandProtocol", + }, + stakewolle: { + url: "https://stakewolle.com", + description: + "Stakewolle community validator and public RPC provider. Runs keyless Tendermint RPC endpoints for multiple Cosmos SDK chains.", + twitter: "@stakewolle", + }, + + // ─── cheqd providers (bench 257) ───────────────────────────────────── + "cheqd-official": { + url: "https://cheqd.io", + description: + "cheqd official public Tendermint RPC node for cheqd-mainnet-1. Keyless endpoint maintained by the cheqd Network team.", + twitter: "@cheqd_io", + }, + nodestake: { + url: "https://nodestake.org", + description: + "NodeStake community validator and public RPC operator. Provides keyless Tendermint RPC endpoints for multiple Cosmos SDK chains.", + twitter: "@NodeStake", + }, + + // ─── Union providers (bench 253) ───────────────────────────────────── "nodes-guru": { url: "https://nodes.guru", diff --git a/src/lib/brand.ts b/src/lib/brand.ts index 0d2d6e050..7ce6a49e1 100644 --- a/src/lib/brand.ts +++ b/src/lib/brand.ts @@ -187,6 +187,14 @@ const BRANDS: Record = { itrocket: { color: "#E53E3E" }, // itrocket red "cosmos-directory": { color: "#1B1B2F", dark: true }, // cosmos directory dark navy + // ─── Band Protocol + cheqd (benches 256-257) ─── + bandchain: { color: "#516AFF" }, // band protocol indigo (official brand) + cheqd: { color: "#00B59C" }, // cheqd teal (official brand) + "band-official": { color: "#516AFF" }, // band official inherits band indigo + "cheqd-official": { color: "#00B59C" }, // cheqd official inherits cheqd teal + stakewolle: { color: "#F97316" }, // stakewolle orange + nodestake: { color: "#8B5CF6" }, // nodestake violet + // ─── Bitcoin Cash chain + providers (bench 244) ─── "bitcoin-cash": { color: "#0AC18E" }, // bch official green bitcore: { color: "#1A1D21", dark: true }, // bitpay dark diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index b5250d683..e893db5ab 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -339,6 +339,10 @@ const RAW: Record = { highstakes: "/logos/highstakes.svg", itrocket: "/logos/itrocket.svg", "cosmos-directory": "/logos/cosmos-directory.svg", + bandchain: "/logos/bandchain.svg", + cheqd: "/logos/cheqd.svg", + stakewolle: "/logos/stakewolle.svg", + nodestake: "/logos/nodestake.svg", // ─── Oracle deviation (bench 025) — additional brand logos ─── // (pairs alias to chain/asset logos in the ALIASES block below) @@ -752,6 +756,8 @@ const ALIASES: Record = { "fetchai-official": "fetchhub", "shentu-official": "shentu", "mantrachain-official": "mantrachain", + "band-official": "bandchain", + "cheqd-official": "cheqd", // Non-EVM wave-3 (benches 222-231) — provider-official aliases to chain slug "ecadinfra": "tezos", From 1d3ad1c11bf4c9a03e58f181c680434f27b01ef7 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:04:48 +0200 Subject: [PATCH 11/66] fix: bench-blob revalidate 300s to fix perp ISR conflict (#2206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * merge: dev → main (Gains carry fix + vault fee split) (#2191) * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * feat(rpc): Union bench #253 (#2195) * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: Gains carry projection (v6 action names + still-open positions) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column for long tickers (xyz:NVDA) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column in top markets to fit xyz:NVDA * feat(rpc): add Union bench #253 (Nodes.Guru, Stake And Relax, High Stakes) (#2194) * fix(union-rpc): remove em dashes from YAML * feat(union): add provider SVG logos and brand colors * feat: Shentu #254 + MANTRA Chain #255 benches * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: Gains carry projection (v6 action names + still-open positions) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column for long tickers (xyz:NVDA) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column in top markets to fit xyz:NVDA * feat(rpc): add Union bench #253 (Nodes.Guru, Stake And Relax, High Stakes) (#2194) * fix(union-rpc): remove em dashes from YAML (#2196) * feat(rpc): Shentu #254 + MANTRA Chain #255 (#2199) * feat(rpc): add Shentu bench #254 and MANTRA Chain bench #255 * fix: remove accidental dev-portal submodule * fix: bench-blob revalidate 300s to fix perp ISR conflict --- public/logos/highstakes.svg | 5 +++++ public/logos/nodes-guru.svg | 5 +++++ public/logos/stakeandrelax.svg | 5 +++++ src/lib/bench-blob.ts | 2 +- 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 public/logos/highstakes.svg create mode 100644 public/logos/nodes-guru.svg create mode 100644 public/logos/stakeandrelax.svg diff --git a/public/logos/highstakes.svg b/public/logos/highstakes.svg new file mode 100644 index 000000000..220f0fad4 --- /dev/null +++ b/public/logos/highstakes.svg @@ -0,0 +1,5 @@ + + + HIGH + STAKES + diff --git a/public/logos/nodes-guru.svg b/public/logos/nodes-guru.svg new file mode 100644 index 000000000..c55d6fd0c --- /dev/null +++ b/public/logos/nodes-guru.svg @@ -0,0 +1,5 @@ + + + NODES + GURU + diff --git a/public/logos/stakeandrelax.svg b/public/logos/stakeandrelax.svg new file mode 100644 index 000000000..3664a52dd --- /dev/null +++ b/public/logos/stakeandrelax.svg @@ -0,0 +1,5 @@ + + + STAKE + RELAX + diff --git a/src/lib/bench-blob.ts b/src/lib/bench-blob.ts index 07f96cde4..45e825e55 100644 --- a/src/lib/bench-blob.ts +++ b/src/lib/bench-blob.ts @@ -54,7 +54,7 @@ async function fetchJson(url: string): Promise { try { const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), - cache: "no-store", + next: { revalidate: 300 }, }); if (!res.ok) return null; return await res.json(); From 3914371bf3e86930711dc83446bcc4892acbc695 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 00:48:14 +0200 Subject: [PATCH 12/66] =?UTF-8?q?fix(fee-compare):=20clearer=20Gains=20lab?= =?UTF-8?q?els=20=E2=80=94=20Position=20size=20+=20vault=20fee=20(#2207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(fee-compare): rename Volume→Position size + clearer vault fee label * fix(fee-compare): show total position value + inline vault fee explanation --- src/components/fee-compare-client.tsx | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/fee-compare-client.tsx b/src/components/fee-compare-client.tsx index a7ada7a71..59c816dd4 100644 --- a/src/components/fee-compare-client.tsx +++ b/src/components/fee-compare-client.tsx @@ -741,24 +741,29 @@ function WalletSide({ return (
-

Volume

+

Total position value

{fmtUsd(volume)}

+

+ Sum of (collateral × leverage) across all trades in the period. +

Taker fees

{fmtUsd(gW.feesUsdc)}

{hasVault && ( -
-

- OI vault fee{" "} - OI imbalance surcharge -

-

- +{fmtUsd(gW.vaultFeesUsdc)} + <> +

+

Vault fee

+

+ +{fmtUsd(gW.vaultFeesUsdc)} +

+
+

+ Gains charges an extra fee to LPs when your trade increases the long/short imbalance. Hyperliquid does not have this — it uses funding rates instead.

-
+ )} {hasFunding && (
@@ -1220,7 +1225,7 @@ function GainsTradeTable({

{fmtUsd(netCost)}

{fmtUsd(t.tradingFee)} taker - {hasVault ? ` +${fmtUsd(t.vaultFee)} vault` : ""} + {hasVault ? ` +${fmtUsd(t.vaultFee)} vault LP` : ""} {t.borrowingFee > 0.001 ? ` +${fmtUsd(t.borrowingFee)} borrow` : ""} {Math.abs(t.fundingFee) > 0.001 ? ` ${t.fundingFee > 0 ? "+" : "−"}${fmtUsd(Math.abs(t.fundingFee))} fund` From f0b1bbde5bddc37e73a48b836510b108a862237e Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 00:48:47 +0200 Subject: [PATCH 13/66] fix(fee-compare): total position value + inline vault fee explanation (#2208) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(fee-compare): rename Volume→Position size + clearer vault fee label * fix(fee-compare): show total position value + inline vault fee explanation From f0ac45f2425aed0a93358c3a82abe82928649393 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 01:07:58 +0200 Subject: [PATCH 14/66] =?UTF-8?q?fix(fee-compare):=20remove=20vault=20fee?= =?UTF-8?q?=20=E2=80=94=20carry=20fees=20settled=20per-action,=20not=20OI?= =?UTF-8?q?=20surcharge=20(#2209)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/fee-compare/route.ts | 27 +++++++++++---------------- src/components/fee-compare-client.tsx | 20 +------------------- 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index b4f9e2e12..d6d533019 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -132,6 +132,7 @@ type GainsApiTrade = { realizedTradingFeesCollateral?: number; realizedFundingFeesCollateral?: number; realizedNewBorrowingFeesCollateral?: number; + realizedOldBorrowingFeesCollateral?: number; }; }; }; @@ -180,8 +181,7 @@ type HlWalletData = { type GainsWalletData = { events: number; - feesUsdc: number; // pure taker fee only (uiRealizedPnlData) - vaultFeesUsdc: number; // OI imbalance vault surcharge (tradeFeesData - uiRealizedPnlData) + feesUsdc: number; fundingFeesUsdc: number; fundingEstimated: boolean; borrowingFeesUsdc: number; @@ -193,8 +193,7 @@ type GainsWalletData = { pair: string; action: string; notional: number; - tradingFee: number; // taker fee only - vaultFee: number; // OI vault surcharge + tradingFee: number; fundingFee: number; borrowingFee: number; equivFee?: number; // equivalent fee on the other venue @@ -1525,23 +1524,20 @@ export async function GET(req: Request) { const otherSlug = slug === venueA ? venueB : venueA; const otherRate = slug === venueA ? rateB : rateA; let feesUsdc = 0; - let vaultFeesUsdc = 0; let fundingFeesUsdc = 0; let borrowingFeesUsdc = 0; let notionalUsd = 0; const recentTrades: GainsWalletData["recentTrades"] = []; for (const t of usdcTrades) { - // uiRealizedPnlData = pure taker fee; tradeFeesData = taker + OI vault surcharge + // Gains settles carry (funding + borrowing) on every action, not just closes. + // uiRealizedPnlData breaks down taker, funding, and borrowing separately — use it for all. const takerFee = t.meta?.uiRealizedPnlData?.realizedTradingFeesCollateral ?? t.meta?.tradeFeesData?.realizedTradingFeesCollateral ?? 0; - const totalTradingFee = t.meta?.tradeFeesData?.realizedTradingFeesCollateral ?? takerFee; - const vaultFee = Math.max(0, totalTradingFee - takerFee); - const isClose = CLOSE_ACTIONS.has(t.action); - const fundingFee = isClose ? (t.meta?.uiRealizedPnlData?.realizedFundingFeesCollateral ?? 0) : 0; - const borrowingFee = isClose ? (t.meta?.uiRealizedPnlData?.realizedNewBorrowingFeesCollateral ?? 0) : 0; + const fundingFee = t.meta?.uiRealizedPnlData?.realizedFundingFeesCollateral ?? 0; + const borrowingFee = (t.meta?.uiRealizedPnlData?.realizedNewBorrowingFeesCollateral ?? 0) + + (t.meta?.uiRealizedPnlData?.realizedOldBorrowingFeesCollateral ?? 0); feesUsdc += takerFee; - vaultFeesUsdc += vaultFee; fundingFeesUsdc += fundingFee; borrowingFeesUsdc += borrowingFee; const tradeNotional = t.size * t.leverage; @@ -1550,7 +1546,7 @@ export async function GET(req: Request) { const equivFee = otherSlug === "hyperliquid" ? tradeNotional * (gainsData.perSide[t.pair.split("/")[0]] ?? otherRate) : tradeNotional * otherRate; - recentTrades.push({ date: t.date, pair: t.pair, action: t.action, notional: tradeNotional, tradingFee: takerFee, vaultFee, fundingFee, borrowingFee, equivFee, pnl_net: t.pnl_net }); + recentTrades.push({ date: t.date, pair: t.pair, action: t.action, notional: tradeNotional, tradingFee: takerFee, fundingFee, borrowingFee, equivFee, pnl_net: t.pnl_net }); } } @@ -1566,17 +1562,16 @@ export async function GET(req: Request) { } } - const netCostUsdc = feesUsdc + vaultFeesUsdc + fundingFeesUsdc + borrowingFeesUsdc; + const netCostUsdc = feesUsdc + fundingFeesUsdc + borrowingFeesUsdc; walletData = { events: usdcTrades.length, feesUsdc, - vaultFeesUsdc, fundingFeesUsdc, fundingEstimated, borrowingFeesUsdc, netCostUsdc, positionSizeUsdc: notionalUsd, - avgFeeRateBps: notionalUsd > 0 ? (feesUsdc / notionalUsd) * 10000 : 0, + avgFeeRateBps: notionalUsd > 0 ? (netCostUsdc / notionalUsd) * 10000 : 0, recentTrades, } satisfies GainsWalletData; } else if (fetchEvmWallet && slug === "gmx-v2" && gmxWalletData) { diff --git a/src/components/fee-compare-client.tsx b/src/components/fee-compare-client.tsx index 59c816dd4..736879dc5 100644 --- a/src/components/fee-compare-client.tsx +++ b/src/components/fee-compare-client.tsx @@ -75,7 +75,6 @@ type HlWalletData = { type GainsWalletData = { events: number; feesUsdc: number; - vaultFeesUsdc: number; fundingFeesUsdc: number; fundingEstimated: boolean; borrowingFeesUsdc: number; @@ -88,7 +87,6 @@ type GainsWalletData = { action: string; notional: number; tradingFee: number; - vaultFee: number; fundingFee: number; borrowingFee: number; equivFee?: number; @@ -735,7 +733,6 @@ function WalletSide({ })()} {venue.slug === "gains" && (() => { const gW = w as GainsWalletData; - const hasVault = (gW.vaultFeesUsdc ?? 0) > 0.5; const hasFunding = Math.abs(gW.fundingFeesUsdc) > 0.5; const hasBorrowing = gW.borrowingFeesUsdc > 0.5; return ( @@ -752,19 +749,6 @@ function WalletSide({

Taker fees

{fmtUsd(gW.feesUsdc)}

- {hasVault && ( - <> -
-

Vault fee

-

- +{fmtUsd(gW.vaultFeesUsdc)} -

-
-

- Gains charges an extra fee to LPs when your trade increases the long/short imbalance. Hyperliquid does not have this — it uses funding rates instead. -

- - )} {hasFunding && (

@@ -1202,8 +1186,7 @@ function GainsTradeTable({ {rows.map((t, i) => { - const hasVault = (t.vaultFee ?? 0) > 0.001; - const netCost = t.tradingFee + (t.vaultFee ?? 0) + t.fundingFee + t.borrowingFee; + const netCost = t.tradingFee + t.fundingFee + t.borrowingFee; const diff = hasEquiv && t.equivFee !== undefined ? t.equivFee - netCost : undefined; return ( @@ -1225,7 +1208,6 @@ function GainsTradeTable({

{fmtUsd(netCost)}

{fmtUsd(t.tradingFee)} taker - {hasVault ? ` +${fmtUsd(t.vaultFee)} vault LP` : ""} {t.borrowingFee > 0.001 ? ` +${fmtUsd(t.borrowingFee)} borrow` : ""} {Math.abs(t.fundingFee) > 0.001 ? ` ${t.fundingFee > 0 ? "+" : "−"}${fmtUsd(Math.abs(t.fundingFee))} fund` From 2197a7d87168d6ee852b7add154394b44a935df0 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:41:05 +0200 Subject: [PATCH 15/66] feat(rpc): ICON bench #258 (#2210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * merge: dev → main (Gains carry fix + vault fee split) (#2191) * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * feat(rpc): Union bench #253 (#2195) * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: Gains carry projection (v6 action names + still-open positions) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column for long tickers (xyz:NVDA) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column in top markets to fit xyz:NVDA * feat(rpc): add Union bench #253 (Nodes.Guru, Stake And Relax, High Stakes) (#2194) * fix(union-rpc): remove em dashes from YAML * feat(union): add provider SVG logos and brand colors * feat: Shentu #254 + MANTRA Chain #255 benches * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: Gains carry projection (v6 action names + still-open positions) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column for long tickers (xyz:NVDA) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column in top markets to fit xyz:NVDA * feat(rpc): add Union bench #253 (Nodes.Guru, Stake And Relax, High Stakes) (#2194) * fix(union-rpc): remove em dashes from YAML (#2196) * feat(rpc): Shentu #254 + MANTRA Chain #255 (#2199) * feat(rpc): add Shentu bench #254 and MANTRA Chain bench #255 * fix: remove accidental dev-portal submodule * feat: Band Protocol #256 + cheqd #257 benches * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: Gains carry projection (v6 action names + still-open positions) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column for long tickers (xyz:NVDA) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column in top markets to fit xyz:NVDA * feat(rpc): add Union bench #253 (Nodes.Guru, Stake And Relax, High Stakes) (#2194) * fix(union-rpc): remove em dashes from YAML (#2196) * feat(rpc): Shentu #254 + MANTRA Chain #255 (#2199) * feat(rpc): add Shentu bench #254 and MANTRA Chain bench #255 * fix: remove accidental dev-portal submodule * feat: Band Protocol #256 + cheqd #257 benches * fix: bench-blob revalidate 300s to fix perp ISR conflict (#2206) * merge: dev → main (Gains carry fix + vault fee split) (#2191) * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * feat(rpc): Union bench #253 (#2195) * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: Gains carry projection (v6 action names + still-open positions) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column for long tickers (xyz:NVDA) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column in top markets to fit xyz:NVDA * feat(rpc): add Union bench #253 (Nodes.Guru, Stake And Relax, High Stakes) (#2194) * fix(union-rpc): remove em dashes from YAML * feat(union): add provider SVG logos and brand colors * feat: Shentu #254 + MANTRA Chain #255 benches * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: Gains carry projection (v6 action names + still-open positions) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column for long tickers (xyz:NVDA) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column in top markets to fit xyz:NVDA * feat(rpc): add Union bench #253 (Nodes.Guru, Stake And Relax, High Stakes) (#2194) * fix(union-rpc): remove em dashes from YAML (#2196) * feat(rpc): Shentu #254 + MANTRA Chain #255 (#2199) * feat(rpc): add Shentu bench #254 and MANTRA Chain bench #255 * fix: remove accidental dev-portal submodule * fix: bench-blob revalidate 300s to fix perp ISR conflict * fix(fee-compare): total position value + inline vault fee explanation * fix(fee-compare): remove vault fee — carry fees settled per-action, not OI surcharge (#2209) * feat(rpc): ICON bench #258 — new icon probe kind, 3 keyless providers --- benchmarks/icon-rpc.yml | 35 +++++++++++ .../rpc-capabilities/cmd/script/config.go | 11 ++++ .../rpc-capabilities/cmd/script/probe.go | 59 ++++++++++++++++++- public/logos/icon.svg | 4 ++ public/logos/iconblockchain.svg | 5 ++ src/data/provider-registry.ts | 18 ++++++ src/lib/brand.ts | 6 ++ src/lib/logo-manifest.ts | 4 ++ 8 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 benchmarks/icon-rpc.yml create mode 100644 public/logos/icon.svg create mode 100644 public/logos/iconblockchain.svg diff --git a/benchmarks/icon-rpc.yml b/benchmarks/icon-rpc.yml new file mode 100644 index 000000000..f45ec814c --- /dev/null +++ b/benchmarks/icon-rpc.yml @@ -0,0 +1,35 @@ +id: 258 +slug: icon-rpc +title: "Fastest free ICON RPC, live no-key endpoint latency" +chain: icon +description: "Latency and availability benchmark for ICON public RPC endpoints" +category: rpc +kind: icon + +providers: + - slug: icon-solidwallet + name: "ICON Foundation" + url: "https://ctz.solidwallet.io" + - slug: icon-community + name: "ICON Community" + url: "https://api.icon.community" + - slug: iconblockchain + name: "iconblockchain.xyz" + url: "https://api.iconblockchain.xyz" + +seo_intro: | + ICON is a South Korean L1 blockchain focused on interoperability and enterprise adoption, using a Delegated Proof of Contribution (DPoC) consensus with ~2-second block finality. ICON nodes expose a JSON-RPC 2.0 API: POST /api/v3 with method icx_getLastBlock returns the latest block height. Free public endpoints are available keyless from the ICON Foundation (ctz.solidwallet.io), ICON Community (api.icon.community), and iconblockchain.xyz. + + This benchmark continuously measures RPC latency, availability and block-height freshness across these public ICON endpoints from three geographic regions. Every provider was live-verified with consecutive keyless icx_getLastBlock probes at launch. + +faq: + - q: "What does this benchmark measure?" + a: "Each probe issues a POST /api/v3 icx_getLastBlock request to retrieve the latest block height. We record round-trip latency (p50/p90/p99), HTTP availability, and whether the returned block height is current. Probes run every 60 seconds from US East, EU West, and AP Southeast." + - q: "Which endpoints are included?" + a: "The benchmark covers the ICON Foundation endpoint (ctz.solidwallet.io), ICON Community (api.icon.community), and iconblockchain.xyz. All three are keyless public endpoints requiring no API key." + - q: "Why does ICON RPC performance matter?" + a: "ICON powers ICX transfers, BTP cross-chain messages, and DApps across the ICON ecosystem. Low-latency RPC access is critical for wallets, DEX aggregators integrating ICON, and validators monitoring chain health." + - q: "How does ICON's ~2-second block time affect staleness detection?" + a: "With blocks every ~2 seconds, a gap of 150 blocks represents roughly 5 minutes of drift — the same threshold we use to classify other fast-finality chains as stale. A provider returning a block more than 150 behind the cross-provider tip is marked stale." + - q: "Can I contribute an endpoint?" + a: "Yes. Open an issue or PR at github.com/ChainBench/OpenChainBench with the endpoint URL and operator name. We verify liveness and independence before adding." diff --git a/harnesses/rpc-capabilities/cmd/script/config.go b/harnesses/rpc-capabilities/cmd/script/config.go index dc35473f2..b61dc0d04 100644 --- a/harnesses/rpc-capabilities/cmd/script/config.go +++ b/harnesses/rpc-capabilities/cmd/script/config.go @@ -1837,6 +1837,17 @@ func chains() []Chain { {Slug: "waves-exchange", Name: "Waves Exchange Node", URL: envDefault("RPC_URL_WAVES_EXCHANGE", "https://nodes.waves.exchange")}, }, }, + // 2026-08-29 wave-13. ICON blockchain — JSON-RPC icx_getLastBlock /api/v3, ~2 s/block. 3 keyless providers. + { + Slug: "icon", + Name: "ICON", + Kind: "icon", + Providers: []Provider{ + {Slug: "icon-solidwallet", Name: "ICON Foundation", URL: envDefault("RPC_URL_ICON_SOLIDWALLET", "https://ctz.solidwallet.io")}, + {Slug: "icon-community", Name: "ICON Community", URL: envDefault("RPC_URL_ICON_COMMUNITY", "https://api.icon.community")}, + {Slug: "iconblockchain", Name: "iconblockchain.xyz", URL: envDefault("RPC_URL_ICON_ICONBLOCKCHAIN", "https://api.iconblockchain.xyz")}, + }, + }, // 2026-08-18 wave-8. WAX gaming blockchain (Antelope) — REST GET /v1/chain/get_info, ~0.5 s/block. 3 keyless providers. { Slug: "wax", diff --git a/harnesses/rpc-capabilities/cmd/script/probe.go b/harnesses/rpc-capabilities/cmd/script/probe.go index 6a2569e29..77a2560cc 100644 --- a/harnesses/rpc-capabilities/cmd/script/probe.go +++ b/harnesses/rpc-capabilities/cmd/script/probe.go @@ -104,6 +104,9 @@ const ( // veChainStaleBlockGap: VeChain produces one block every ~10 s, // so 30 blocks ≈ 5 min. veChainStaleBlockGap uint64 = 30 + // iconStaleBlockGap: ICON produces one block every ~2 s, + // so 150 blocks ≈ 5 min. + iconStaleBlockGap uint64 = 150 ) // chainTips tracks the highest block seen for each chain across all @@ -328,6 +331,8 @@ func probeOne(ctx context.Context, c Chain, p Provider) { block, result, latency, err = callWavesBlock(probeCtx, p.URL) case "vechain": block, result, latency, err = callVeChainBlock(probeCtx, p.URL) + case "icon": + block, result, latency, err = callICONBlock(probeCtx, p.URL) default: block, hash, result, latency, err = callLatestBlock(probeCtx, p.URL) } @@ -385,6 +390,8 @@ func probeOne(ctx context.Context, c Chain, p Provider) { gap = wavesStaleBlockGap case "vechain": gap = veChainStaleBlockGap + case "icon": + gap = iconStaleBlockGap } if tip > 0 && block+gap < tip { result = "stale" @@ -406,7 +413,7 @@ func probeOne(ctx context.Context, c Chain, p Provider) { case "solana", "polkadot", "cosmos", "starknet", "stellar", "sui", "aptos", "xrpl", "algorand", "gram", "near", "flow", "hedera", "ckb", "multiversx", "neo", - "tezos", "antelope", "waves", "vechain", "dogecoin", "zcash", "bitcoin-cash", "litecoin": + "tezos", "antelope", "waves", "vechain", "icon", "dogecoin", "zcash", "bitcoin-cash", "litecoin": // no consensus participation default: if result == "ok" || result == "stale" { @@ -1465,6 +1472,56 @@ func callVeChainBlock(ctx context.Context, url string) (blockNum uint64, result return blk.Number, "ok", latencyMs, nil } +type iconLastBlock struct { + Result struct { + Height int64 `json:"height"` + } `json:"result"` +} + +// callICONBlock probes an ICON node via POST /api/v3 icx_getLastBlock. +// Returns the block height. Staleness uses iconStaleBlockGap in probeOne. +func callICONBlock(ctx context.Context, url string) (height uint64, result string, latencyMs float64, err error) { + target := strings.TrimRight(url, "/") + "/api/v3" + body := []byte(fmt.Sprintf( + `{"jsonrpc":"2.0","method":"icx_getLastBlock","id":%d,"params":{}}`, + time.Now().UnixNano(), + )) + req, _ := http.NewRequestWithContext(ctx, "POST", target, bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "OpenChainBench/1.0 (+https://openchainbench.com)") + client := &http.Client{Timeout: probeTimeout} + + start := time.Now() + resp, err := client.Do(req) + latencyMs = float64(time.Since(start).Nanoseconds()) / 1e6 + + if err != nil { + if ctx.Err() != nil || strings.Contains(err.Error(), "deadline exceeded") || strings.Contains(err.Error(), "Timeout") { + return 0, "timeout", latencyMs, err + } + return 0, "http_err", latencyMs, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + _, _ = io.Copy(io.Discard, resp.Body) + return 0, "http_err", latencyMs, fmt.Errorf("status %d", resp.StatusCode) + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return 0, "http_err", latencyMs, err + } + var blk iconLastBlock + if err := json.Unmarshal(raw, &blk); err != nil { + return 0, "http_err", latencyMs, err + } + if blk.Result.Height <= 0 { + return 0, "jsonrpc_err", latencyMs, fmt.Errorf("icon block missing height") + } + return uint64(blk.Result.Height), "ok", latencyMs, nil +} + // callNeoBlockCount probes a NEO N3 node via getblockcount JSON-RPC. // The result is a plain decimal integer. Staleness uses neoStaleBlockGap in probeOne. func callNeoBlockCount(ctx context.Context, url string) (count uint64, result string, latencyMs float64, err error) { diff --git a/public/logos/icon.svg b/public/logos/icon.svg new file mode 100644 index 000000000..5d65cf529 --- /dev/null +++ b/public/logos/icon.svg @@ -0,0 +1,4 @@ + + + ICON + diff --git a/public/logos/iconblockchain.svg b/public/logos/iconblockchain.svg new file mode 100644 index 000000000..ca414b9c0 --- /dev/null +++ b/public/logos/iconblockchain.svg @@ -0,0 +1,5 @@ + + + ICON + BLOCKCHAIN + diff --git a/src/data/provider-registry.ts b/src/data/provider-registry.ts index dd0f2bd4e..afdb90338 100644 --- a/src/data/provider-registry.ts +++ b/src/data/provider-registry.ts @@ -2672,6 +2672,24 @@ export const PROVIDER_REGISTRY: Record = { }, + // ─── ICON providers (bench 258) ────────────────────────────────────── + "icon-solidwallet": { + url: "https://www.icondev.io", + description: + "ICON Foundation official public RPC node at ctz.solidwallet.io. Keyless icx_getLastBlock endpoint maintained by the ICON Foundation.", + twitter: "@helloiconworld", + }, + "icon-community": { + url: "https://icon.community", + description: + "ICON Community public RPC at api.icon.community. Keyless icx_getLastBlock endpoint run by the ICON community.", + twitter: "@helloiconworld", + }, + iconblockchain: { + url: "https://iconblockchain.xyz", + description: + "iconblockchain.xyz community-operated public ICON RPC node. Keyless icx_getLastBlock endpoint.", + }, // ─── Union providers (bench 253) ───────────────────────────────────── "nodes-guru": { url: "https://nodes.guru", diff --git a/src/lib/brand.ts b/src/lib/brand.ts index 7ce6a49e1..b5d76ef28 100644 --- a/src/lib/brand.ts +++ b/src/lib/brand.ts @@ -195,6 +195,12 @@ const BRANDS: Record = { stakewolle: { color: "#F97316" }, // stakewolle orange nodestake: { color: "#8B5CF6" }, // nodestake violet + // ─── ICON (bench 258) ─── + icon: { color: "#00B8CC" }, // ICON teal (official brand) + "icon-solidwallet": { color: "#00B8CC" }, // ICON Foundation inherits brand teal + "icon-community": { color: "#1A9CBB" }, // ICON Community slightly darker teal + iconblockchain: { color: "#0D7A9E" }, // iconblockchain.xyz dark cyan + // ─── Bitcoin Cash chain + providers (bench 244) ─── "bitcoin-cash": { color: "#0AC18E" }, // bch official green bitcore: { color: "#1A1D21", dark: true }, // bitpay dark diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index e893db5ab..9c3f74ba7 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -343,6 +343,8 @@ const RAW: Record = { cheqd: "/logos/cheqd.svg", stakewolle: "/logos/stakewolle.svg", nodestake: "/logos/nodestake.svg", + icon: "/logos/icon.svg", + iconblockchain: "/logos/iconblockchain.svg", // ─── Oracle deviation (bench 025) — additional brand logos ─── // (pairs alias to chain/asset logos in the ALIASES block below) @@ -758,6 +760,8 @@ const ALIASES: Record = { "mantrachain-official": "mantrachain", "band-official": "bandchain", "cheqd-official": "cheqd", + "icon-solidwallet": "icon", + "icon-community": "icon", // Non-EVM wave-3 (benches 222-231) — provider-official aliases to chain slug "ecadinfra": "tezos", From 6fa26c195351b9810e4698c2e21ce55df6671417 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:53:10 +0200 Subject: [PATCH 16/66] fix(rpc): rewrite bench YAMLs 256-258 with proper SpecSchema format --- benchmarks/bandchain-rpc.yml | 159 +++++++++++++++++++++++++++++------ benchmarks/cheqd-rpc.yml | 159 +++++++++++++++++++++++++++++------ benchmarks/icon-rpc.yml | 159 +++++++++++++++++++++++++++++------ 3 files changed, 393 insertions(+), 84 deletions(-) diff --git a/benchmarks/bandchain-rpc.yml b/benchmarks/bandchain-rpc.yml index 67a1c01ff..249bbb896 100644 --- a/benchmarks/bandchain-rpc.yml +++ b/benchmarks/bandchain-rpc.yml @@ -1,35 +1,138 @@ -id: 256 +# OpenChainBench. Bench No 256 + slug: bandchain-rpc -title: "Band Protocol RPC" -chain: bandchain -description: "Latency and availability benchmark for Band Protocol public RPC endpoints" -category: rpc -kind: cosmos +number: "256" +title: Fastest free Band Protocol RPC, live no-key endpoint latency +seo_title: "Fastest free Band Protocol RPC 2026" +seo_description: "{{best_name}} leads free Band Protocol RPC at {{best_p50}} (block height p50, 24h). 3 no-key providers measured every 60s from 3 regions." +subtitle: HTTP round-trip latency for Tendermint /status queries against every available public Band Protocol (laozi-mainnet) endpoint, audited every 60 seconds from 3 regions. -providers: - - slug: band-official - name: "Band Protocol" - url: "http://rpc.laozi1.bandchain.org:80" - - slug: highstakes - name: "High Stakes" - url: "https://bandprotocol-rpc.highstakes.ch" - - slug: stakewolle - name: "Stakewolle" - url: "https://public.stakewolle.com/cosmos/bandchain/rpc" +category: RPCs +status: live +metric: RPC latency +unit: ms +higher_is_better: false seo_intro: | - Band Protocol is a cross-chain oracle network built on Cosmos SDK that aggregates and connects real-world data and APIs to smart contracts. The laozi-mainnet hosts its decentralized data oracle infrastructure, enabling DeFi protocols across multiple blockchains to access tamper-proof price feeds. + Band Protocol is a cross-chain decentralized oracle network built on Cosmos SDK (chain ID laozi-mainnet). It aggregates and connects real-world data and APIs to smart contracts across multiple blockchains via its Yoda oracle daemon and data feed infrastructure. The chain uses the standard Tendermint RPC interface; block height is fetched via the /status endpoint. Public keyless RPC nodes are provided by the Band Protocol official team, High Stakes, and Stakewolle. Every provider was live-verified with consecutive block-height probes at launch. - This benchmark continuously measures RPC latency, availability and block-height freshness across public Band Protocol Tendermint endpoints from three geographic regions. Use it to select the fastest endpoint for your integration or validator setup. +abstract: | + Per-chain member of the RPC latency cluster, extended to Band Protocol (laozi-mainnet). + We measure the round-trip latency of a Tendermint /status query against + every available public Band Protocol endpoint that sustains continuous probing: + 3 providers at launch, every 60 seconds, from us-east, eu-west and + Singapore. Each provider is probed via a GET /status request + from which the block number is extracted. + The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) + with a Cosmos-scaled staleness gap (40 blocks, around 4 min at 6 s/block). + The cross-chain view lives on the parent rpc-capabilities benchmark; + this page is the Band Protocol-scoped answer with per-region breakdowns as + a first-class dimension. + +methodology: + - "Cadence: every 60 seconds per provider, from each of 3 probe regions (us-east Virginia, eu-west Amsterdam, sgp Singapore). Headline p50/p90/p99 aggregate across all 3 regions via Prometheus avg(quantile_over_time(...)); per-region breakdowns are first-class on this page via the region tabs." + - "Payload: GET /status. The result.sync_info.latest_block_height field is extracted as the current block height." + - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus quantile_over_time over the last 24 hours." + - "Call-result classification: ok (parsable block height returned), http_err, jsonrpc_err, stale (block more than 40 behind the cross-provider tip), timeout." + - "This page is part of the per-chain RPC cluster derived from the cross-chain rpc-capabilities benchmark; the identical harness, cadence and exclusion rules apply on every chain." + - "Chain scope: every query on this page is pinned to chain=bandchain. Provider coverage at launch: 3 endpoints (Band Protocol official, High Stakes, Stakewolle)." + +findings: + - "{{best_name}} leads free Band Protocol RPC at {{best_p50}} (Tendermint /status p50, 24h) across 3 measured providers." faq: - - q: "What does this benchmark measure?" - a: "Each probe issues a GET /status request with anti-cache headers to retrieve the latest block height. We record round-trip latency (p50/p90/p99), HTTP availability, and whether the returned block height is current. Probes run every 30 seconds from US East, EU West, and AP Southeast." - - q: "Which endpoints are included?" - a: "The benchmark covers the Band Protocol official endpoint (rpc.laozi1.bandchain.org), High Stakes, and Stakewolle. All three are keyless public endpoints requiring no authentication." - - q: "Why does Band Protocol RPC performance matter?" - a: "Band Protocol validators and oracle scripts depend on reliable RPC access to submit data requests and retrieve oracle results. DeFi protocols integrating Band price feeds need low-latency RPC for real-time data consumption." - - q: "How are oracle scripts affected by RPC latency?" - a: "Yoda (the oracle daemon) and Bothan (data proxy) both rely on RPC to monitor pending data requests and submit responses within the request window. High latency or downtime directly reduces oracle reliability and can cause missed rewards." - - q: "Can I contribute an endpoint?" - a: "Yes. Open an issue or PR at github.com/ChainBench/OpenChainBench with the endpoint URL and operator name. We verify liveness and independence before adding." + - q: "What is the fastest free Band Protocol RPC right now?" + a: "{{best_name}} currently leads at {{best_p50}} (Band Protocol block height p50 over the last 24h), measured against 3 providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + - q: "Which Band Protocol RPC endpoints work without an API key?" + a: "3 endpoints sustain continuous keyless probing at launch: Band Protocol official (rpc.laozi1.bandchain.org), High Stakes (bandprotocol-rpc.highstakes.ch), and Stakewolle (public.stakewolle.com/cosmos/bandchain/rpc). Every listed endpoint was live-verified before inclusion." + - q: "What is Band Protocol and why does its RPC latency matter?" + a: "Band Protocol is a cross-chain data oracle that aggregates real-world data feeds and delivers them to smart contracts via oracle scripts. Validators running the Yoda daemon and DeFi protocols consuming Band price feeds both depend on low-latency reliable RPC access to the Band chain." + - q: "Does the fastest Band Protocol RPC change by region?" + a: "Yes. The official Band node and community validators are hosted in different regions. The region tabs re-scope every number to a single probe origin so you can pick the best endpoint for your deployment geography." + - q: "What is the BAND token?" + a: "BAND is the native staking and governance token of Band Protocol (denominated as uband on-chain). It is used to stake as validators, pay for oracle data requests, and participate in on-chain governance of the laozi-mainnet." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities + +prometheus: + window: 24h + freshness_metric: rpc_latency_milliseconds + +rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="bandchain"}) + +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: eu-west, label: EU-West } + - { value: sgp, label: Singapore } + +providers: + - slug: band-official + name: Band Protocol + tag: Band Protocol official public RPC node, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to rpc.laozi1.bandchain.org." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="band-official", chain="bandchain"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="band-official", chain="bandchain"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="band-official", chain="bandchain"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="band-official", chain="bandchain"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="band-official", chain="bandchain"}) / sum(ocb:rpc_call:rate_24h{provider="band-official", chain="bandchain"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="band-official", chain="bandchain"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="band-official", chain="bandchain"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="band-official", chain="bandchain", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="band-official", chain="bandchain", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="band-official", chain="bandchain", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="band-official", chain="bandchain", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="band-official", chain="bandchain", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="band-official", chain="bandchain", region="sgp"}[1h]) + + - slug: highstakes + name: High Stakes + tag: High Stakes public Band Protocol RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to bandprotocol-rpc.highstakes.ch." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="bandchain"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="highstakes", chain="bandchain"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="highstakes", chain="bandchain"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="highstakes", chain="bandchain"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="highstakes", chain="bandchain"}) / sum(ocb:rpc_call:rate_24h{provider="highstakes", chain="bandchain"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="highstakes", chain="bandchain"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="bandchain"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="bandchain", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="bandchain", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="bandchain", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="bandchain", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="highstakes", chain="bandchain", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="highstakes", chain="bandchain", region="sgp"}[1h]) + + - slug: stakewolle + name: Stakewolle + tag: Stakewolle public Band Protocol RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to public.stakewolle.com/cosmos/bandchain/rpc." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="stakewolle", chain="bandchain"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="stakewolle", chain="bandchain"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="stakewolle", chain="bandchain"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="stakewolle", chain="bandchain"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="stakewolle", chain="bandchain"}) / sum(ocb:rpc_call:rate_24h{provider="stakewolle", chain="bandchain"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="stakewolle", chain="bandchain"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="stakewolle", chain="bandchain"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="stakewolle", chain="bandchain", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="stakewolle", chain="bandchain", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="stakewolle", chain="bandchain", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="stakewolle", chain="bandchain", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="stakewolle", chain="bandchain", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="stakewolle", chain="bandchain", region="sgp"}[1h]) diff --git a/benchmarks/cheqd-rpc.yml b/benchmarks/cheqd-rpc.yml index c5d028836..19752bf17 100644 --- a/benchmarks/cheqd-rpc.yml +++ b/benchmarks/cheqd-rpc.yml @@ -1,35 +1,138 @@ -id: 257 +# OpenChainBench. Bench No 257 + slug: cheqd-rpc -title: "cheqd RPC" -chain: cheqd -description: "Latency and availability benchmark for cheqd public RPC endpoints" -category: rpc -kind: cosmos +number: "257" +title: Fastest free cheqd RPC, live no-key endpoint latency +seo_title: "Fastest free cheqd RPC 2026" +seo_description: "{{best_name}} leads free cheqd RPC at {{best_p50}} (block height p50, 24h). 3 no-key providers measured every 60s from 3 regions." +subtitle: HTTP round-trip latency for Tendermint /status queries against every available public cheqd (cheqd-mainnet-1) endpoint, audited every 60 seconds from 3 regions. -providers: - - slug: cheqd-official - name: "cheqd" - url: "https://rpc.cheqd.net" - - slug: publicnode - name: "PublicNode" - url: "https://cheqd-rpc.publicnode.com:443" - - slug: nodestake - name: "NodeStake" - url: "https://rpc.cheqd.nodestake.org" +category: RPCs +status: live +metric: RPC latency +unit: ms +higher_is_better: false seo_intro: | - cheqd is a purpose-built Cosmos SDK blockchain for decentralized identity, enabling self-sovereign identity (SSI) and verifiable credentials at scale. The cheqd-mainnet-1 network anchors DIDs and credential schemas used by enterprises, governments, and developers building trust infrastructure. + cheqd is a purpose-built Cosmos SDK blockchain (chain ID cheqd-mainnet-1) for decentralized identity, enabling self-sovereign identity (SSI) and verifiable credentials at scale. The cheqd-mainnet-1 network anchors DIDs and credential schemas used by enterprises, governments, and developers building trust infrastructure. The chain uses the standard Tendermint RPC interface; block height is fetched via the /status endpoint. Public keyless RPC nodes are provided by the cheqd official team, PublicNode, and NodeStake. Every provider was live-verified with consecutive block-height probes at launch. - This benchmark continuously measures RPC latency, availability and block-height freshness across public cheqd Tendermint endpoints from three geographic regions. Use it to select the most reliable endpoint for identity resolution, node operation, or application integration. +abstract: | + Per-chain member of the RPC latency cluster, extended to cheqd (cheqd-mainnet-1). + We measure the round-trip latency of a Tendermint /status query against + every available public cheqd endpoint that sustains continuous probing: + 3 providers at launch, every 60 seconds, from us-east, eu-west and + Singapore. Each provider is probed via a GET /status request + from which the block number is extracted. + The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) + with a Cosmos-scaled staleness gap (40 blocks, around 4 min at 6 s/block). + The cross-chain view lives on the parent rpc-capabilities benchmark; + this page is the cheqd-scoped answer with per-region breakdowns as + a first-class dimension. + +methodology: + - "Cadence: every 60 seconds per provider, from each of 3 probe regions (us-east Virginia, eu-west Amsterdam, sgp Singapore). Headline p50/p90/p99 aggregate across all 3 regions via Prometheus avg(quantile_over_time(...)); per-region breakdowns are first-class on this page via the region tabs." + - "Payload: GET /status. The result.sync_info.latest_block_height field is extracted as the current block height." + - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus quantile_over_time over the last 24 hours." + - "Call-result classification: ok (parsable block height returned), http_err, jsonrpc_err, stale (block more than 40 behind the cross-provider tip), timeout." + - "This page is part of the per-chain RPC cluster derived from the cross-chain rpc-capabilities benchmark; the identical harness, cadence and exclusion rules apply on every chain." + - "Chain scope: every query on this page is pinned to chain=cheqd. Provider coverage at launch: 3 endpoints (cheqd official, PublicNode, NodeStake)." + +findings: + - "{{best_name}} leads free cheqd RPC at {{best_p50}} (Tendermint /status p50, 24h) across 3 measured providers." faq: - - q: "What does this benchmark measure?" - a: "Each probe issues a GET /status request with anti-cache headers to retrieve the latest block height. We record round-trip latency (p50/p90/p99), HTTP availability, and whether the returned block height is current. Probes run every 30 seconds from US East, EU West, and AP Southeast." - - q: "Which endpoints are included?" - a: "The benchmark covers the official cheqd endpoint (rpc.cheqd.net), PublicNode, and NodeStake. All three are keyless public endpoints requiring no API key." - - q: "Why does cheqd RPC performance matter?" - a: "DID resolution, verifiable credential anchoring, and CHEQ token transfers all depend on RPC availability. Applications using the cheqd DID method resolve identifiers via RPC, making latency directly visible to end users." - - q: "How does RPC latency affect DID resolution?" - a: "The Universal Resolver and cheqd-specific resolvers query RPC endpoints to fetch DID documents. Slow or unavailable endpoints increase credential verification times and can break SSI flows in production." - - q: "Can I contribute an endpoint?" - a: "Yes. Open an issue or PR at github.com/ChainBench/OpenChainBench with the endpoint URL and operator name. We verify liveness and independence before adding." + - q: "What is the fastest free cheqd RPC right now?" + a: "{{best_name}} currently leads at {{best_p50}} (cheqd block height p50 over the last 24h), measured against 3 providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + - q: "Which cheqd RPC endpoints work without an API key?" + a: "3 endpoints sustain continuous keyless probing at launch: cheqd official (rpc.cheqd.net), PublicNode (cheqd-rpc.publicnode.com), and NodeStake (rpc.cheqd.nodestake.org). Every listed endpoint was live-verified before inclusion." + - q: "What is cheqd and why does its RPC latency matter?" + a: "cheqd is a Cosmos SDK blockchain built for decentralized identity (DID) and verifiable credentials. DID resolution, verifiable credential anchoring, and CHEQ token transfers all depend on RPC availability. Applications using the cheqd DID method resolve identifiers via RPC, making latency directly visible to end users and credential verifiers." + - q: "Does the fastest cheqd RPC change by region?" + a: "Yes. The official cheqd node and community validators are hosted in different regions. The region tabs re-scope every number to a single probe origin so you can pick the best endpoint for your deployment geography." + - q: "What is the CHEQ token?" + a: "CHEQ is the native staking and governance token of the cheqd network (denominated as ncheq on-chain). It is used to pay for DID writes, credential schema anchoring, and on-chain governance participation." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities + +prometheus: + window: 24h + freshness_metric: rpc_latency_milliseconds + +rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="cheqd"}) + +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: eu-west, label: EU-West } + - { value: sgp, label: Singapore } + +providers: + - slug: cheqd-official + name: cheqd + tag: cheqd official public RPC node, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to rpc.cheqd.net." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="cheqd-official", chain="cheqd"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="cheqd-official", chain="cheqd"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="cheqd-official", chain="cheqd"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="cheqd-official", chain="cheqd"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="cheqd-official", chain="cheqd"}) / sum(ocb:rpc_call:rate_24h{provider="cheqd-official", chain="cheqd"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="cheqd-official", chain="cheqd"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="cheqd-official", chain="cheqd"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="cheqd-official", chain="cheqd", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="cheqd-official", chain="cheqd", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="cheqd-official", chain="cheqd", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="cheqd-official", chain="cheqd", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="cheqd-official", chain="cheqd", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="cheqd-official", chain="cheqd", region="sgp"}[1h]) + + - slug: publicnode + name: PublicNode + tag: PublicNode public cheqd RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to cheqd-rpc.publicnode.com." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="cheqd"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="publicnode", chain="cheqd"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="publicnode", chain="cheqd"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="publicnode", chain="cheqd"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="publicnode", chain="cheqd"}) / sum(ocb:rpc_call:rate_24h{provider="publicnode", chain="cheqd"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="publicnode", chain="cheqd"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="cheqd"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="cheqd", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="cheqd", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="cheqd", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="cheqd", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="cheqd", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="cheqd", region="sgp"}[1h]) + + - slug: nodestake + name: NodeStake + tag: NodeStake public cheqd RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET /status sent every 60s from 3 regions to rpc.cheqd.nodestake.org." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="nodestake", chain="cheqd"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="nodestake", chain="cheqd"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="nodestake", chain="cheqd"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="nodestake", chain="cheqd"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="nodestake", chain="cheqd"}) / sum(ocb:rpc_call:rate_24h{provider="nodestake", chain="cheqd"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="nodestake", chain="cheqd"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="nodestake", chain="cheqd"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="nodestake", chain="cheqd", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="nodestake", chain="cheqd", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="nodestake", chain="cheqd", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="nodestake", chain="cheqd", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="nodestake", chain="cheqd", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="nodestake", chain="cheqd", region="sgp"}[1h]) diff --git a/benchmarks/icon-rpc.yml b/benchmarks/icon-rpc.yml index f45ec814c..a04d9938b 100644 --- a/benchmarks/icon-rpc.yml +++ b/benchmarks/icon-rpc.yml @@ -1,35 +1,138 @@ -id: 258 +# OpenChainBench. Bench No 258 + slug: icon-rpc -title: "Fastest free ICON RPC, live no-key endpoint latency" -chain: icon -description: "Latency and availability benchmark for ICON public RPC endpoints" -category: rpc -kind: icon +number: "258" +title: Fastest free ICON RPC, live no-key endpoint latency +seo_title: "Fastest free ICON RPC 2026" +seo_description: "{{best_name}} leads free ICON RPC at {{best_p50}} (block height p50, 24h). 3 no-key providers measured every 60s from 3 regions." +subtitle: HTTP round-trip latency for icx_getLastBlock queries against every available public ICON blockchain endpoint, audited every 60 seconds from 3 regions. -providers: - - slug: icon-solidwallet - name: "ICON Foundation" - url: "https://ctz.solidwallet.io" - - slug: icon-community - name: "ICON Community" - url: "https://api.icon.community" - - slug: iconblockchain - name: "iconblockchain.xyz" - url: "https://api.iconblockchain.xyz" +category: RPCs +status: live +metric: RPC latency +unit: ms +higher_is_better: false seo_intro: | - ICON is a South Korean L1 blockchain focused on interoperability and enterprise adoption, using a Delegated Proof of Contribution (DPoC) consensus with ~2-second block finality. ICON nodes expose a JSON-RPC 2.0 API: POST /api/v3 with method icx_getLastBlock returns the latest block height. Free public endpoints are available keyless from the ICON Foundation (ctz.solidwallet.io), ICON Community (api.icon.community), and iconblockchain.xyz. + ICON is a South Korean L1 blockchain focused on interoperability and enterprise adoption, using a Delegated Proof of Contribution (DPoC) consensus with roughly 2-second block finality. ICON nodes expose a JSON-RPC 2.0 API: POST /api/v3 with method icx_getLastBlock returns the latest block height as a decimal integer. Free public endpoints are available keyless from the ICON Foundation (ctz.solidwallet.io), ICON Community (api.icon.community), and iconblockchain.xyz. Every provider was live-verified with consecutive block-height probes at launch. - This benchmark continuously measures RPC latency, availability and block-height freshness across these public ICON endpoints from three geographic regions. Every provider was live-verified with consecutive keyless icx_getLastBlock probes at launch. +abstract: | + Per-chain member of the RPC latency cluster, extended to ICON (ICX). + We measure the round-trip latency of a JSON-RPC icx_getLastBlock call against + every available public ICON endpoint that sustains continuous probing: + 3 providers at launch, every 60 seconds, from us-east, eu-west and + Singapore. Each provider is probed via a POST /api/v3 request + from which the block height is extracted from result.height. + The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) + with an ICON-scaled staleness gap (150 blocks, around 5 min at 2 s/block). + The cross-chain view lives on the parent rpc-capabilities benchmark; + this page is the ICON-scoped answer with per-region breakdowns as + a first-class dimension. + +methodology: + - "Cadence: every 60 seconds per provider, from each of 3 probe regions (us-east Virginia, eu-west Amsterdam, sgp Singapore). Headline p50/p90/p99 aggregate across all 3 regions via Prometheus avg(quantile_over_time(...)); per-region breakdowns are first-class on this page via the region tabs." + - "Payload: POST /api/v3 with method icx_getLastBlock. The result.height field (decimal integer) is extracted as the current block height." + - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus quantile_over_time over the last 24 hours." + - "Call-result classification: ok (parsable block height returned), http_err, jsonrpc_err, stale (block more than 150 behind the cross-provider tip, roughly 5 min at ICON's 2 s block time), timeout." + - "This page is part of the per-chain RPC cluster derived from the cross-chain rpc-capabilities benchmark; the identical harness, cadence and exclusion rules apply on every chain." + - "Chain scope: every query on this page is pinned to chain=icon. Provider coverage at launch: 3 endpoints (ICON Foundation, ICON Community, iconblockchain.xyz)." + +findings: + - "{{best_name}} leads free ICON RPC at {{best_p50}} (icx_getLastBlock p50, 24h) across 3 measured providers." faq: - - q: "What does this benchmark measure?" - a: "Each probe issues a POST /api/v3 icx_getLastBlock request to retrieve the latest block height. We record round-trip latency (p50/p90/p99), HTTP availability, and whether the returned block height is current. Probes run every 60 seconds from US East, EU West, and AP Southeast." - - q: "Which endpoints are included?" - a: "The benchmark covers the ICON Foundation endpoint (ctz.solidwallet.io), ICON Community (api.icon.community), and iconblockchain.xyz. All three are keyless public endpoints requiring no API key." - - q: "Why does ICON RPC performance matter?" - a: "ICON powers ICX transfers, BTP cross-chain messages, and DApps across the ICON ecosystem. Low-latency RPC access is critical for wallets, DEX aggregators integrating ICON, and validators monitoring chain health." - - q: "How does ICON's ~2-second block time affect staleness detection?" - a: "With blocks every ~2 seconds, a gap of 150 blocks represents roughly 5 minutes of drift — the same threshold we use to classify other fast-finality chains as stale. A provider returning a block more than 150 behind the cross-provider tip is marked stale." - - q: "Can I contribute an endpoint?" - a: "Yes. Open an issue or PR at github.com/ChainBench/OpenChainBench with the endpoint URL and operator name. We verify liveness and independence before adding." + - q: "What is the fastest free ICON RPC right now?" + a: "{{best_name}} currently leads at {{best_p50}} (ICON block height p50 over the last 24h), measured against 3 providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + - q: "Which ICON RPC endpoints work without an API key?" + a: "3 endpoints sustain continuous keyless probing at launch: ICON Foundation (ctz.solidwallet.io), ICON Community (api.icon.community), and iconblockchain.xyz. Every listed endpoint was live-verified before inclusion." + - q: "What is ICON and why does its RPC latency matter?" + a: "ICON is a South Korean blockchain focused on enterprise interoperability, originally built for connecting financial institutions and public organizations. ICX wallets, DApp integrations, and BTP cross-chain bridging all depend on reliable RPC access. Low latency matters for real-time ICX transfers and DeFi protocols built on the ICON ecosystem." + - q: "Does the fastest ICON RPC change by region?" + a: "Yes. The ICON Foundation node and community operators are distributed globally. The region tabs re-scope every number to a single probe origin so you can pick the best endpoint for your deployment geography." + - q: "What is the ICX token?" + a: "ICX is the native staking and governance token of the ICON network. It is used to pay transaction fees, stake with validators (P-Reps), and participate in on-chain governance. ICON uses a Delegated Proof of Contribution model where P-Reps are elected by ICX holders." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities + +prometheus: + window: 24h + freshness_metric: rpc_latency_milliseconds + +rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="icon"}) + +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: eu-west, label: EU-West } + - { value: sgp, label: Singapore } + +providers: + - slug: icon-solidwallet + name: ICON Foundation + tag: ICON Foundation official public RPC node, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a POST /api/v3 icx_getLastBlock sent every 60s from 3 regions to ctz.solidwallet.io." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="icon-solidwallet", chain="icon"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="icon-solidwallet", chain="icon"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="icon-solidwallet", chain="icon"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="icon-solidwallet", chain="icon"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="icon-solidwallet", chain="icon"}) / sum(ocb:rpc_call:rate_24h{provider="icon-solidwallet", chain="icon"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="icon-solidwallet", chain="icon"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="icon-solidwallet", chain="icon"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="icon-solidwallet", chain="icon", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="icon-solidwallet", chain="icon", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="icon-solidwallet", chain="icon", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="icon-solidwallet", chain="icon", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="icon-solidwallet", chain="icon", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="icon-solidwallet", chain="icon", region="sgp"}[1h]) + + - slug: icon-community + name: ICON Community + tag: ICON Community public RPC node, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a POST /api/v3 icx_getLastBlock sent every 60s from 3 regions to api.icon.community." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="icon-community", chain="icon"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="icon-community", chain="icon"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="icon-community", chain="icon"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="icon-community", chain="icon"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="icon-community", chain="icon"}) / sum(ocb:rpc_call:rate_24h{provider="icon-community", chain="icon"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="icon-community", chain="icon"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="icon-community", chain="icon"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="icon-community", chain="icon", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="icon-community", chain="icon", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="icon-community", chain="icon", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="icon-community", chain="icon", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="icon-community", chain="icon", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="icon-community", chain="icon", region="sgp"}[1h]) + + - slug: iconblockchain + name: iconblockchain.xyz + tag: iconblockchain.xyz community-operated public ICON RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a POST /api/v3 icx_getLastBlock sent every 60s from 3 regions to api.iconblockchain.xyz." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="iconblockchain", chain="icon"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="iconblockchain", chain="icon"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="iconblockchain", chain="icon"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="iconblockchain", chain="icon"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="iconblockchain", chain="icon"}) / sum(ocb:rpc_call:rate_24h{provider="iconblockchain", chain="icon"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="iconblockchain", chain="icon"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="iconblockchain", chain="icon"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="iconblockchain", chain="icon", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="iconblockchain", chain="icon", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="iconblockchain", chain="icon", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="iconblockchain", chain="icon", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="iconblockchain", chain="icon", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="iconblockchain", chain="icon", region="sgp"}[1h]) From 12bcb869a3ffe20f3256cec2356f5c07806de573 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:08:20 +0200 Subject: [PATCH 17/66] fix: show signed HL funding projection, rename Volume to Total position value, clarify vault fee label (#2212) --- src/app/api/fee-compare/route.ts | 13 +++++++------ src/components/fee-compare-client.tsx | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index d6d533019..dde1e296e 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -1260,7 +1260,8 @@ async function fetchHlFundingHistory( } // Compute projected HL funding cost for a set of position slices. -// Uses absolute funding rates so direction doesn't matter for the projection. +// Returns signed net: positive = wallet pays, negative = wallet receives. +// Rate > 0 = longs pay; < 0 = shorts pay (HL convention). function computeHlFunding( positions: PositionSlice[], history: Map> @@ -1270,10 +1271,10 @@ function computeHlFunding( const rates = (history.get(pos.coin) ?? []).filter( (r) => r.time >= pos.openMs && r.time <= pos.closeMs ); - // Each HL funding entry = one 8h interval. Rate > 0 = longs pay; < 0 = shorts pay. for (const r of rates) { + // positive cost = this position is on the paying side const cost = pos.isLong ? r.rate : -r.rate; - total += pos.notionalUsd * Math.max(0, cost); + total += pos.notionalUsd * cost; } } return total; @@ -1683,7 +1684,7 @@ export async function GET(req: Request) { borrowFees: 0, fundingFees: hlFunding, borrowProjected: false, - fundingProjected: hlFunding > 0.01, + fundingProjected: Math.abs(hlFunding) > 0.01, }; } @@ -1821,9 +1822,9 @@ export async function GET(req: Request) { projectedCarry = { takerFees, borrowFees: 0, - fundingFees: hlFunding, + fundingFees: hlFunding, // signed: positive = pays, negative = receives borrowProjected: false, - fundingProjected: hlFunding > 0.01, + fundingProjected: Math.abs(hlFunding) > 0.01, }; } diff --git a/src/components/fee-compare-client.tsx b/src/components/fee-compare-client.tsx index be2404676..ce765c3f7 100644 --- a/src/components/fee-compare-client.tsx +++ b/src/components/fee-compare-client.tsx @@ -560,7 +560,7 @@ function WalletSide({ if (crossSim) { const carry = crossSim.projectedCarry; const takerFees = carry ? carry.takerFees : crossSim.equivFees; - const hasCarry = carry && (carry.borrowFees > 0.01 || carry.fundingFees > 0.01); + const hasCarry = carry && (carry.borrowFees > 0.01 || Math.abs(carry.fundingFees) > 0.01); const netLabel = hasCarry ? "incl. est. carry" : carry @@ -602,18 +602,24 @@ function WalletSide({

+{fmtUsd(carry.borrowFees)}

)} - {carry && carry.borrowProjected === false && carry.fundingFees < 0.01 && ( + {carry && carry.borrowProjected === false && Math.abs(carry.fundingFees) < 0.01 && (

Borrowing fees

not applicable

)} - {carry && carry.fundingFees > 0.01 && ( + {carry && carry.fundingProjected && carry.fundingFees > 0.01 && (

Est. funding (projected)

+{fmtUsd(carry.fundingFees)}

)} + {carry && carry.fundingProjected && carry.fundingFees < -0.01 && ( +
+

Est. funding (projected)

+

−{fmtUsd(Math.abs(carry.fundingFees))} received

+
+ )} {carry && !carry.fundingProjected && carry.borrowFees > 0.01 && (

Funding

@@ -691,11 +697,11 @@ function WalletSide({ {/* Venue-specific extra stats */} {venue.slug === "hyperliquid" && (() => { const hlW = w as HlWalletData; - const hasFunding = Math.abs(hlW.fundingUsd) > 0.5; + const hasFunding = Math.abs(hlW.fundingUsd) > 0.01; return (
-

Volume

+

Total position value

{fmtUsd(volume)}

@@ -766,7 +772,7 @@ function WalletSide({ )} {hasBorrowing && (
-

Borrowing fees

+

Borrowing fees (vault)

−{fmtUsd(gW.borrowingFeesUsdc)}

From 2fb8d6308d7314df31c2391ae03ca71881d565d2 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:13:41 +0200 Subject: [PATCH 18/66] fix: Gains position reconstruction for HL funding projection (#2213) * fix: include long-running Gains positions in HL funding projection * fix: use earliest POSSIZEINCREASE as anchor for positions opened before fetch window --- src/app/api/fee-compare/route.ts | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index dde1e296e..44d728778 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -1115,31 +1115,40 @@ function reconstructGainsPositions(trades: GainsApiTrade[], cutoffMs: number): P const INCREASE_ACTIONS = new Set(["TradePosSizeIncrease"]); const CLOSE_ACTIONS = new Set(["TradeClosedMarket", "TradeClosedTP", "TradeClosedSL", "TradeClosedLIQ"]); - const byId = new Map(); + const byId = new Map(); for (const t of trades) { if (!byId.has(t.id)) byId.set(t.id, {}); const e = byId.get(t.id)!; if (OPEN_ACTIONS.has(t.action)) e.open = t; - else if (INCREASE_ACTIONS.has(t.action) && e.open) { - e.open = { ...e.open, size: t.size, leverage: t.leverage }; + else if (INCREASE_ACTIONS.has(t.action)) { + if (e.open) e.open = { ...e.open, size: t.size, leverage: t.leverage }; + // Track increases for positions opened before the window (no open event in data) + else if (!e.lastIncrease || new Date(t.date).getTime() < new Date(e.lastIncrease.date).getTime()) { + e.lastIncrease = t; + } } else if (CLOSE_ACTIONS.has(t.action)) e.close = t; } const now = Date.now(); const slices: PositionSlice[] = []; - for (const { open, close } of byId.values()) { - if (!open) continue; - const openMs = new Date(open.date).getTime(); - if (openMs < cutoffMs) continue; - // Still-open positions use now as close time (same as reconstructHlPositions) + for (const { open, close, lastIncrease } of byId.values()) { + // Use open event if available; fall back to earliest increase in the window + // for positions opened before the fetch window (open event not in data). + const anchor = open ?? lastIncrease; + if (!anchor) continue; + const rawOpenMs = new Date(anchor.date).getTime(); const closeMs = close ? new Date(close.date).getTime() : now; + // Skip positions that closed before the analysis window + if (closeMs < cutoffMs) continue; + // Cap openMs to the window start so long-running positions aren't missed + const openMs = Math.max(rawOpenMs, cutoffMs); slices.push({ - coin: open.pair.split("/")[0], - notionalUsd: open.size * open.leverage, + coin: anchor.pair.split("/")[0], + notionalUsd: anchor.size * anchor.leverage, openMs, closeMs, - isLong: open.buy !== false, + isLong: anchor.buy !== false, }); } From e7037ef8d2ffc9e036897de86358653644dc7b17 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:19:44 +0200 Subject: [PATCH 19/66] fix: 1yr Gains lookback for accurate HL funding projection (#2214) * fix: include long-running Gains positions in HL funding projection * fix: use earliest POSSIZEINCREASE as anchor for positions opened before fetch window * fix: fetch 1yr Gains history for position reconstruction to find true open timestamps --- src/app/api/fee-compare/route.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index 44d728778..940facd8f 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -1490,8 +1490,11 @@ export async function GET(req: Request) { await Promise.all(fetches); - // Phase 2: fetch HL funding history for Gains positions (Gains→HL carry projection) + // Phase 2: fetch HL funding history + extended Gains history for position reconstruction let hlFundingHistoryByCoins: Map> = new Map(); + // Extended Gains history (1 year) used only for HL funding projection reconstruction — + // the fee accounting (taker/borrow/funding fees) still uses gainsTradesData (cutoffMs window). + let gainsPositionData: GainsApiTrade[] = gainsTradesData; if ( fetchEvmWallet && (venueA === "gains" || venueB === "gains") && @@ -1504,7 +1507,13 @@ export async function GET(req: Request) { .map((t) => t.pair.split("/")[0]) ); const coinsToFetch = [...gainsCoinSet].slice(0, 6); - hlFundingHistoryByCoins = await fetchHlFundingHistory(coinsToFetch, cutoffMs).catch(() => new Map()); + const extendedCutoffMs = cutoffMs - 365 * 24 * 60 * 60 * 1000; + const [fundingHistory, extendedTrades] = await Promise.all([ + fetchHlFundingHistory(coinsToFetch, cutoffMs).catch(() => new Map>()), + fetchGainsTrades(wallet, extendedCutoffMs).catch(() => gainsTradesData), + ]); + hlFundingHistoryByCoins = fundingHistory; + gainsPositionData = extendedTrades; } function buildVenueResult(slug: string, rate: number, note: string, rateIsLive: boolean): VenueResult { @@ -1673,9 +1682,9 @@ export async function GET(req: Request) { hlOpenPositions, cutoffMs ); - } else if (venueA === "gains" && gainsTradesData.length > 0) { + } else if (venueA === "gains" && gainsPositionData.length > 0) { positions = reconstructGainsPositions( - gainsTradesData.filter((t) => t.collateralIndex === 3), + gainsPositionData.filter((t) => t.collateralIndex === 3), cutoffMs ); } else if (venueA === "gmx-v2" && gmxWalletData) { @@ -1813,9 +1822,9 @@ export async function GET(req: Request) { hlOpenPositions, cutoffMs ); - } else if (venueB === "gains" && gainsTradesData.length > 0) { + } else if (venueB === "gains" && gainsPositionData.length > 0) { positions = reconstructGainsPositions( - gainsTradesData.filter((t) => t.collateralIndex === 3), + gainsPositionData.filter((t) => t.collateralIndex === 3), cutoffMs ); } else if (venueB === "gmx-v2" && gmxWalletData) { From 70f44e4c7dec7995272e789d5da1b558335aee36 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 20:01:07 +0200 Subject: [PATCH 20/66] feat: apples-to-apples fee comparison (HL-comparable coins only) * fix: include long-running Gains positions in HL funding projection * fix: use earliest POSSIZEINCREASE as anchor for positions opened before fetch window * fix: fetch 1yr Gains history for position reconstruction to find true open timestamps * feat: grey out Gains-exclusive pairs in comparison, exclude from HL fee calc * fix: hide equivFee for Gains-exclusive pairs (coin not on HL) --- src/app/api/fee-compare/route.ts | 49 ++++++++++++++++++++++----- src/components/fee-compare-client.tsx | 18 ++++++++-- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index 940facd8f..3e2a63df2 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -188,6 +188,7 @@ type GainsWalletData = { netCostUsdc: number; positionSizeUsdc: number; avgFeeRateBps: number; + gainsExclusiveFeesUsdc?: number; // fees on coins not available on the other venue recentTrades: Array<{ date: string; pair: string; @@ -196,7 +197,8 @@ type GainsWalletData = { tradingFee: number; fundingFee: number; borrowingFee: number; - equivFee?: number; // equivalent fee on the other venue + equivFee?: number; + hlComparable?: boolean; // false = coin not listed on HL pnl_net: number; }>; }; @@ -1352,14 +1354,16 @@ function toChecksumAddress(address: string): string { return "0x" + result; } -function walletStats(slug: string, w: AnyWallet): { notional: number; fees: number } | null { +function walletStats(slug: string, w: AnyWallet, otherSlug?: string): { notional: number; fees: number } | null { if (slug === "hyperliquid") { const x = w as HlWalletData; return x.fills > 0 ? { notional: x.notionalUsd, fees: x.netCostUsd } : null; } if (slug === "gains") { const x = w as GainsWalletData; - return x.events > 0 ? { notional: x.positionSizeUsdc, fees: x.netCostUsdc } : null; + // When comparing against HL, exclude fees on coins not available on HL + const exclusiveFees = otherSlug === "hyperliquid" ? (x.gainsExclusiveFeesUsdc ?? 0) : 0; + return x.events > 0 ? { notional: x.positionSizeUsdc, fees: x.netCostUsdc - exclusiveFees } : null; } if (slug === "gmx-v2") { const x = w as GmxWalletData; @@ -1439,6 +1443,7 @@ export async function GET(req: Request) { let hlFillsData: HlFill[] = []; let hlFundingData: HlFundingEvent[] = []; let hlOpenPositions: HlOpenPos[] = []; + let hlAvailableCoins = new Set(); let gainsTradesData: GainsApiTrade[] = []; let gmxWalletData: GmxWalletData | null = null; let dydxWalletData: DydxWalletData | null = null; @@ -1466,6 +1471,21 @@ export async function GET(req: Request) { fetches.push( fetchGainsTrades(wallet, cutoffMs).then((d) => { gainsTradesData = d; }).catch(() => {}) ); + if (venueA === "hyperliquid" || venueB === "hyperliquid") { + fetches.push( + fetch(HL_API, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type: "meta" }), + signal: AbortSignal.timeout(5000), + }) + .then((r) => r.json()) + .then((d: { universe: Array<{ name: string }> }) => { + hlAvailableCoins = new Set(d.universe.map((c) => c.name)); + }) + .catch(() => {}) + ); + } } if (venueA === "gmx-v2" || venueB === "gmx-v2") { fetches.push( @@ -1546,6 +1566,8 @@ export async function GET(req: Request) { let fundingFeesUsdc = 0; let borrowingFeesUsdc = 0; let notionalUsd = 0; + let gainsExclusiveFeesUsdc = 0; + const checkHlComparable = otherSlug === "hyperliquid" && hlAvailableCoins.size > 0; const recentTrades: GainsWalletData["recentTrades"] = []; for (const t of usdcTrades) { @@ -1561,11 +1583,19 @@ export async function GET(req: Request) { borrowingFeesUsdc += borrowingFee; const tradeNotional = t.size * t.leverage; notionalUsd += tradeNotional; + const coin = t.pair.split("/")[0]; + const hlComparable = checkHlComparable ? hlAvailableCoins.has(coin) : undefined; + if (hlComparable === false) { + gainsExclusiveFeesUsdc += takerFee + fundingFee + borrowingFee; + } if (recentTrades.length < 50) { - const equivFee = otherSlug === "hyperliquid" - ? tradeNotional * (gainsData.perSide[t.pair.split("/")[0]] ?? otherRate) - : tradeNotional * otherRate; - recentTrades.push({ date: t.date, pair: t.pair, action: t.action, notional: tradeNotional, tradingFee: takerFee, fundingFee, borrowingFee, equivFee, pnl_net: t.pnl_net }); + // Don't show equivFee for Gains-exclusive coins — the coin doesn't exist on HL + const equivFee = hlComparable === false + ? undefined + : otherSlug === "hyperliquid" + ? tradeNotional * (gainsData.perSide[coin] ?? otherRate) + : tradeNotional * otherRate; + recentTrades.push({ date: t.date, pair: t.pair, action: t.action, notional: tradeNotional, tradingFee: takerFee, fundingFee, borrowingFee, equivFee, hlComparable, pnl_net: t.pnl_net }); } } @@ -1591,6 +1621,7 @@ export async function GET(req: Request) { netCostUsdc, positionSizeUsdc: notionalUsd, avgFeeRateBps: notionalUsd > 0 ? (netCostUsdc / notionalUsd) * 10000 : 0, + gainsExclusiveFeesUsdc: checkHlComparable ? gainsExclusiveFeesUsdc : undefined, recentTrades, } satisfies GainsWalletData; } else if (fetchEvmWallet && slug === "gmx-v2" && gmxWalletData) { @@ -1669,7 +1700,7 @@ export async function GET(req: Request) { } } } else { - const stats = walletStats(venueA, venueAResult.wallet); + const stats = walletStats(venueA, venueAResult.wallet, venueB); if (stats) { let equivFees = stats.notional * rateB; let projectedCarry: SimResult["projectedCarry"]; @@ -1809,7 +1840,7 @@ export async function GET(req: Request) { } } } else { - const stats = walletStats(venueB, venueBResult.wallet); + const stats = walletStats(venueB, venueBResult.wallet, venueA); if (stats) { let equivFees = stats.notional * rateA; let projectedCarry: SimResult["projectedCarry"]; diff --git a/src/components/fee-compare-client.tsx b/src/components/fee-compare-client.tsx index ce765c3f7..d73f7e752 100644 --- a/src/components/fee-compare-client.tsx +++ b/src/components/fee-compare-client.tsx @@ -81,6 +81,7 @@ type GainsWalletData = { netCostUsdc: number; positionSizeUsdc: number; avgFeeRateBps: number; + gainsExclusiveFeesUsdc?: number; recentTrades: Array<{ date: string; pair: string; @@ -90,6 +91,7 @@ type GainsWalletData = { fundingFee: number; borrowingFee: number; equivFee?: number; + hlComparable?: boolean; pnl_net: number; }>; }; @@ -786,6 +788,11 @@ function WalletSide({ {fmtUsd(gW.netCostUsdc)}

+ {(gW.gainsExclusiveFeesUsdc ?? 0) > 0.01 && ( +

+ Incl. {fmtUsd(gW.gainsExclusiveFeesUsdc!)} on pairs not listed on {otherVenue.name} — excluded from comparison. +

+ )}
); @@ -1152,6 +1159,7 @@ function GainsTradeTable({ const PREVIEW = 10; const rows = showAll ? trades : trades.slice(0, PREVIEW); const hasEquiv = !!otherVenueName && trades.some((t) => t.equivFee !== undefined); + const hasComparability = trades.some((t) => t.hlComparable !== undefined); if (trades.length === 0) return null; @@ -1194,12 +1202,18 @@ function GainsTradeTable({ {rows.map((t, i) => { const netCost = t.tradingFee + t.fundingFee + t.borrowingFee; const diff = hasEquiv && t.equivFee !== undefined ? t.equivFee - netCost : undefined; + const isExclusive = hasComparability && t.hlComparable === false; return ( - + {new Date(t.date).toLocaleString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })} - {t.pair.replace("/USD", "")} + + {t.pair.replace("/USD", "")} + {isExclusive && ( + Gains only + )} + Date: Sun, 30 Aug 2026 20:24:17 +0200 Subject: [PATCH 21/66] fix: HL funding projection accuracy + equivFee + sort + coin filter * fix: signed HL funding projection, filter exclusive coins, increase coin limit to 20 * fix: sort Gains trades oldest-first for correct size tracking; fix equivFee uses HL rate not Gains rate --- src/app/api/fee-compare/route.ts | 35 +++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index 3e2a63df2..547ce358f 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -1117,14 +1117,17 @@ function reconstructGainsPositions(trades: GainsApiTrade[], cutoffMs: number): P const INCREASE_ACTIONS = new Set(["TradePosSizeIncrease"]); const CLOSE_ACTIONS = new Set(["TradeClosedMarket", "TradeClosedTP", "TradeClosedSL", "TradeClosedLIQ"]); + // Sort oldest-first so increases run after the open event and correctly update notional + const sorted = [...trades].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + const byId = new Map(); - for (const t of trades) { + for (const t of sorted) { if (!byId.has(t.id)) byId.set(t.id, {}); const e = byId.get(t.id)!; if (OPEN_ACTIONS.has(t.action)) e.open = t; else if (INCREASE_ACTIONS.has(t.action)) { if (e.open) e.open = { ...e.open, size: t.size, leverage: t.leverage }; - // Track increases for positions opened before the window (no open event in data) + // Track the earliest increase for positions opened before the window (no open event in data) else if (!e.lastIncrease || new Date(t.date).getTime() < new Date(e.lastIncrease.date).getTime()) { e.lastIncrease = t; } @@ -1271,8 +1274,7 @@ async function fetchHlFundingHistory( } // Compute projected HL funding cost for a set of position slices. -// Returns signed net: positive = wallet pays, negative = wallet receives. -// Rate > 0 = longs pay; < 0 = shorts pay (HL convention). +// Uses absolute funding rates so direction doesn't matter for the projection. function computeHlFunding( positions: PositionSlice[], history: Map> @@ -1282,8 +1284,9 @@ function computeHlFunding( const rates = (history.get(pos.coin) ?? []).filter( (r) => r.time >= pos.openMs && r.time <= pos.closeMs ); + // Each HL funding entry = one 8h interval. Rate > 0 = longs pay; < 0 = shorts pay. for (const r of rates) { - // positive cost = this position is on the paying side + // Signed: positive = wallet pays, negative = wallet receives funding const cost = pos.isLong ? r.rate : -r.rate; total += pos.notionalUsd * cost; } @@ -1524,9 +1527,11 @@ export async function GET(req: Request) { const gainsCoinSet = new Set( gainsTradesData .filter((t) => t.collateralIndex === 3) + // Only fetch funding history for coins that actually exist on HL + .filter((t) => hlAvailableCoins.size === 0 || hlAvailableCoins.has(t.pair.split("/")[0])) .map((t) => t.pair.split("/")[0]) ); - const coinsToFetch = [...gainsCoinSet].slice(0, 6); + const coinsToFetch = [...gainsCoinSet].slice(0, 20); const extendedCutoffMs = cutoffMs - 365 * 24 * 60 * 60 * 1000; const [fundingHistory, extendedTrades] = await Promise.all([ fetchHlFundingHistory(coinsToFetch, cutoffMs).catch(() => new Map>()), @@ -1590,11 +1595,11 @@ export async function GET(req: Request) { } if (recentTrades.length < 50) { // Don't show equivFee for Gains-exclusive coins — the coin doesn't exist on HL + // equivFee = what the other venue would charge for this same notional. + // HL has a uniform taker rate (no per-coin lookup); Gains has per-coin rates. const equivFee = hlComparable === false ? undefined - : otherSlug === "hyperliquid" - ? tradeNotional * (gainsData.perSide[coin] ?? otherRate) - : tradeNotional * otherRate; + : tradeNotional * otherRate; recentTrades.push({ date: t.date, pair: t.pair, action: t.action, notional: tradeNotional, tradingFee: takerFee, fundingFee, borrowingFee, equivFee, hlComparable, pnl_net: t.pnl_net }); } } @@ -1715,7 +1720,10 @@ export async function GET(req: Request) { ); } else if (venueA === "gains" && gainsPositionData.length > 0) { positions = reconstructGainsPositions( - gainsPositionData.filter((t) => t.collateralIndex === 3), + gainsPositionData.filter( + (t) => t.collateralIndex === 3 && + (hlAvailableCoins.size === 0 || hlAvailableCoins.has(t.pair.split("/")[0])) + ), cutoffMs ); } else if (venueA === "gmx-v2" && gmxWalletData) { @@ -1855,7 +1863,10 @@ export async function GET(req: Request) { ); } else if (venueB === "gains" && gainsPositionData.length > 0) { positions = reconstructGainsPositions( - gainsPositionData.filter((t) => t.collateralIndex === 3), + gainsPositionData.filter( + (t) => t.collateralIndex === 3 && + (hlAvailableCoins.size === 0 || hlAvailableCoins.has(t.pair.split("/")[0])) + ), cutoffMs ); } else if (venueB === "gmx-v2" && gmxWalletData) { @@ -1871,7 +1882,7 @@ export async function GET(req: Request) { projectedCarry = { takerFees, borrowFees: 0, - fundingFees: hlFunding, // signed: positive = pays, negative = receives + fundingFees: hlFunding, borrowProjected: false, fundingProjected: Math.abs(hlFunding) > 0.01, }; From ac1dc4ab828844bf5cf8f918bfbd1cfa240a87ab Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:32:44 +0200 Subject: [PATCH 22/66] fix(citation): allow negative p50 for deviation benches (#2197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * merge: dev → main (Gains carry fix + vault fee split) (#2191) * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * feat(rpc): Union bench #253 (#2195) * fix(harnesses): cap LZ/CCIP/Axelar cardinality to bench-tracked chains only (#2187) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit (#2188) Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection (#2189) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains action names v5→v6, handle position size increases (#2190) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: Gains carry projection (v6 action names + still-open positions) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column for long tickers (xyz:NVDA) * fix(cache): strip heavy fields from aggregate blob to pass 2MB limit Hub pages only need card data. Drop seriesByRegion24h, metricPanels, editorial text (seoIntro/faq/findings/methodology/disclaimer/abstract/ perChainExplainer) from the blob aggregate slim. Cuts serialized size from ~4.3 MB to under 2 MB so unstable_cache actually persists the entry instead of silently dropping it on every render. * fix: include still-open Gains positions in carry projection * fix: update Gains open action names to v6 API, handle TradePosSizeIncrease * fix: widen coin name column in top markets to fit xyz:NVDA * feat(rpc): add Union bench #253 (Nodes.Guru, Stake And Relax, High Stakes) (#2194) * fix(union-rpc): remove em dashes from YAML * feat(union): add provider SVG logos and brand colors * fix(citation): allow negative p50 for deviation benches --- src/lib/citation.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/citation.ts b/src/lib/citation.ts index 672811f94..f52e76857 100644 --- a/src/lib/citation.ts +++ b/src/lib/citation.ts @@ -302,9 +302,12 @@ export function isInsufficient(b: InsufficientCheckInput): boolean { // benches as insufficient on /api/citable while /api/stat returned // live values for the same slug. The liveResults length and p50 // finiteness checks below already catch the genuine empty case. + // Use isFinite rather than > 0 so deviation benches (where a negative + // p50 is valid data, e.g. rwa-yield-accuracy reporting -6 bps) are not + // mis-classified as insufficient. const live = b.results.filter( - (r) => r.availability !== "unavailable" && r.ms.p50 > 0, + (r) => r.availability !== "unavailable" && Number.isFinite(r.ms.p50) && r.ms.p50 !== 0, ); if (live.length === 0) return true; - return live.every((r) => !Number.isFinite(r.ms.p50) || r.ms.p50 <= 0); + return live.every((r) => !Number.isFinite(r.ms.p50)); } From ac3504b56c916ec9cb2aaa73be0015c6f4d4bef9 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:50:55 +0200 Subject: [PATCH 23/66] =?UTF-8?q?fix:=20fee-compare=20accuracy=20=E2=80=94?= =?UTF-8?q?=20paginated=20HL=20funding,=20multi-slice=20positions,=20compa?= =?UTF-8?q?rable=20notional=20(#2217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F1: paginate fetchHlFundingHistory (up to 5 pages × 500) to cover 180d+ windows F4: multi-slice reconstructGainsPositions — one PositionSlice per size-change period so pre-increase periods use smaller notional (more accurate HL equiv funding) F5: track comparableNotionalUsdc (HL-listed coins only) and use it in walletStats so the HL taker-equiv fee isn't inflated by PONS/Gains-exclusive notional --- src/app/api/fee-compare/route.ts | 130 ++++++++++++++++++-------- src/components/fee-compare-client.tsx | 1 + 2 files changed, 90 insertions(+), 41 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index 547ce358f..79ea186a9 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -189,6 +189,7 @@ type GainsWalletData = { positionSizeUsdc: number; avgFeeRateBps: number; gainsExclusiveFeesUsdc?: number; // fees on coins not available on the other venue + comparableNotionalUsdc?: number; // notional of HL-comparable trades only recentTrades: Array<{ date: string; pair: string; @@ -1111,50 +1112,80 @@ function augmentWithHlOpenPositions( } function reconstructGainsPositions(trades: GainsApiTrade[], cutoffMs: number): PositionSlice[] { - // v5 names: MarketOpened, LimitOrderExecuted — v6 names: TradeOpenedMarket, TradeOpenedLimit const OPEN_ACTIONS = new Set(["MarketOpened", "LimitOrderExecuted", "TradeOpenedMarket", "TradeOpenedLimit"]); - // TradePosSizeIncrease updates the position size; use latest size as notional const INCREASE_ACTIONS = new Set(["TradePosSizeIncrease"]); const CLOSE_ACTIONS = new Set(["TradeClosedMarket", "TradeClosedTP", "TradeClosedSL", "TradeClosedLIQ"]); - // Sort oldest-first so increases run after the open event and correctly update notional + // Sort oldest-first so increases appear after their open event const sorted = [...trades].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); - const byId = new Map(); + type Entry = { + open?: GainsApiTrade; + close?: GainsApiTrade; + increases: GainsApiTrade[]; + lastIncrease?: GainsApiTrade; // earliest increase, fallback anchor for pre-window positions + }; + const byId = new Map(); + for (const t of sorted) { - if (!byId.has(t.id)) byId.set(t.id, {}); + if (!byId.has(t.id)) byId.set(t.id, { increases: [] }); const e = byId.get(t.id)!; - if (OPEN_ACTIONS.has(t.action)) e.open = t; - else if (INCREASE_ACTIONS.has(t.action)) { - if (e.open) e.open = { ...e.open, size: t.size, leverage: t.leverage }; - // Track the earliest increase for positions opened before the window (no open event in data) - else if (!e.lastIncrease || new Date(t.date).getTime() < new Date(e.lastIncrease.date).getTime()) { - e.lastIncrease = t; + if (OPEN_ACTIONS.has(t.action)) { + e.open = t; + } else if (INCREASE_ACTIONS.has(t.action)) { + e.increases.push(t); + if (!e.open) { + // Track earliest increase as anchor for positions opened before the window + if (!e.lastIncrease || new Date(t.date).getTime() < new Date(e.lastIncrease.date).getTime()) { + e.lastIncrease = t; + } } + } else if (CLOSE_ACTIONS.has(t.action)) { + e.close = t; } - else if (CLOSE_ACTIONS.has(t.action)) e.close = t; } const now = Date.now(); const slices: PositionSlice[] = []; - for (const { open, close, lastIncrease } of byId.values()) { - // Use open event if available; fall back to earliest increase in the window - // for positions opened before the fetch window (open event not in data). + + for (const { open, close, increases, lastIncrease } of byId.values()) { const anchor = open ?? lastIncrease; if (!anchor) continue; - const rawOpenMs = new Date(anchor.date).getTime(); + const closeMs = close ? new Date(close.date).getTime() : now; - // Skip positions that closed before the analysis window if (closeMs < cutoffMs) continue; - // Cap openMs to the window start so long-running positions aren't missed - const openMs = Math.max(rawOpenMs, cutoffMs); - slices.push({ - coin: anchor.pair.split("/")[0], - notionalUsd: anchor.size * anchor.leverage, - openMs, - closeMs, - isLong: anchor.buy !== false, - }); + + const isLong = anchor.buy !== false; + const coin = anchor.pair.split("/")[0]; + + // Build a size timeline: each entry = { ms, notionalUsd } when size changed. + // This lets us create one funding slice per size period instead of one for the whole position. + const timeline: Array<{ ms: number; notionalUsd: number }> = [ + { ms: new Date(anchor.date).getTime(), notionalUsd: anchor.size * anchor.leverage }, + ]; + for (const inc of increases) { + const incMs = new Date(inc.date).getTime(); + // Only track increases that happened after the anchor (skip pre-anchor increases already folded in) + if (incMs > new Date(anchor.date).getTime()) { + timeline.push({ ms: incMs, notionalUsd: inc.size * inc.leverage }); + } + } + // Already sorted oldest-first since increases was pushed in order + + // Emit one slice per size period + for (let i = 0; i < timeline.length; i++) { + const sliceOpen = Math.max(timeline[i].ms, cutoffMs); + const sliceClose = i + 1 < timeline.length ? timeline[i + 1].ms : closeMs; + if (sliceClose <= cutoffMs) continue; // period entirely before window + if (sliceOpen >= sliceClose) continue; // zero-duration + slices.push({ + coin, + notionalUsd: timeline[i].notionalUsd, + openMs: sliceOpen, + closeMs: sliceClose, + isLong, + }); + } } return slices; @@ -1241,25 +1272,34 @@ function estimateGmxBorrowFees( } // Fetch HL 8h funding rate history for a set of coins over a period. -// Returns map of coin → array of { time, rate (as fraction) }. +// Paginates automatically: the HL API returns at most 500 entries per request. +// At 3 entries/day, 500 covers ~167 days. Windows >167d need multiple pages. async function fetchHlFundingHistory( coins: string[], startMs: number ): Promise>> { + const now = Date.now(); const results = await Promise.allSettled( coins.map(async (coin) => { - const res = await fetch(HL_API, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ type: "fundingHistory", coin, startTime: startMs }), - signal: AbortSignal.timeout(8000), - }); - if (!res.ok) return [coin, []] as [string, Array<{ time: number; rate: number }>]; - const data = (await res.json()) as Array<{ time: number; fundingRate: string }>; - return [coin, data.map((d) => ({ time: d.time, rate: parseFloat(d.fundingRate) }))] as [ - string, - Array<{ time: number; rate: number }> - ]; + const rates: Array<{ time: number; rate: number }> = []; + let cursor = startMs; + for (let page = 0; page < 5; page++) { + const res = await fetch(HL_API, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type: "fundingHistory", coin, startTime: cursor }), + signal: AbortSignal.timeout(8000), + }); + if (!res.ok) break; + const data = (await res.json()) as Array<{ time: number; fundingRate: string }>; + if (!Array.isArray(data) || data.length === 0) break; + rates.push(...data.map((d) => ({ time: d.time, rate: parseFloat(d.fundingRate) }))); + // If the response is truncated (exactly 500), fetch the next page + if (data.length < 500) break; + cursor = data[data.length - 1].time + 1; + if (cursor >= now) break; + } + return [coin, rates] as [string, Array<{ time: number; rate: number }>]; }) ); @@ -1364,9 +1404,13 @@ function walletStats(slug: string, w: AnyWallet, otherSlug?: string): { notional } if (slug === "gains") { const x = w as GainsWalletData; - // When comparing against HL, exclude fees on coins not available on HL + // When comparing against HL: exclude exclusive fees AND use comparable-only notional + // so the HL equiv fee isn't inflated by PONS/other non-HL notional const exclusiveFees = otherSlug === "hyperliquid" ? (x.gainsExclusiveFeesUsdc ?? 0) : 0; - return x.events > 0 ? { notional: x.positionSizeUsdc, fees: x.netCostUsdc - exclusiveFees } : null; + const notional = (otherSlug === "hyperliquid" && x.comparableNotionalUsdc !== undefined) + ? x.comparableNotionalUsdc + : x.positionSizeUsdc; + return x.events > 0 ? { notional, fees: x.netCostUsdc - exclusiveFees } : null; } if (slug === "gmx-v2") { const x = w as GmxWalletData; @@ -1571,6 +1615,7 @@ export async function GET(req: Request) { let fundingFeesUsdc = 0; let borrowingFeesUsdc = 0; let notionalUsd = 0; + let comparableNotionalUsdc = 0; let gainsExclusiveFeesUsdc = 0; const checkHlComparable = otherSlug === "hyperliquid" && hlAvailableCoins.size > 0; const recentTrades: GainsWalletData["recentTrades"] = []; @@ -1592,6 +1637,8 @@ export async function GET(req: Request) { const hlComparable = checkHlComparable ? hlAvailableCoins.has(coin) : undefined; if (hlComparable === false) { gainsExclusiveFeesUsdc += takerFee + fundingFee + borrowingFee; + } else { + comparableNotionalUsdc += tradeNotional; } if (recentTrades.length < 50) { // Don't show equivFee for Gains-exclusive coins — the coin doesn't exist on HL @@ -1627,6 +1674,7 @@ export async function GET(req: Request) { positionSizeUsdc: notionalUsd, avgFeeRateBps: notionalUsd > 0 ? (netCostUsdc / notionalUsd) * 10000 : 0, gainsExclusiveFeesUsdc: checkHlComparable ? gainsExclusiveFeesUsdc : undefined, + comparableNotionalUsdc: checkHlComparable ? comparableNotionalUsdc : undefined, recentTrades, } satisfies GainsWalletData; } else if (fetchEvmWallet && slug === "gmx-v2" && gmxWalletData) { diff --git a/src/components/fee-compare-client.tsx b/src/components/fee-compare-client.tsx index d73f7e752..2cf2b04c2 100644 --- a/src/components/fee-compare-client.tsx +++ b/src/components/fee-compare-client.tsx @@ -82,6 +82,7 @@ type GainsWalletData = { positionSizeUsdc: number; avgFeeRateBps: number; gainsExclusiveFeesUsdc?: number; + comparableNotionalUsdc?: number; recentTrades: Array<{ date: string; pair: string; From 84be8675c9f34ff6c467f7d90be306b2908fe707 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:16:46 +0200 Subject: [PATCH 24/66] report: fastest Robinhood Chain RPC (Chainstack vs Alchemy vs QuickNode) --- .../2026-08-fastest-robinhood-chain-rpc.mdx | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/content/reports/rpc/2026-08-fastest-robinhood-chain-rpc.mdx diff --git a/src/content/reports/rpc/2026-08-fastest-robinhood-chain-rpc.mdx b/src/content/reports/rpc/2026-08-fastest-robinhood-chain-rpc.mdx new file mode 100644 index 000000000..b3727fff8 --- /dev/null +++ b/src/content/reports/rpc/2026-08-fastest-robinhood-chain-rpc.mdx @@ -0,0 +1,67 @@ +--- +title: "Fastest Robinhood Chain RPC in 2026: Chainstack vs Alchemy vs QuickNode" +category: "rpc" +slug: "2026-08-fastest-robinhood-chain-rpc" +publishedAt: "2026-08-31" +period: "August 2026" +summary: "Robinhood Chain is live, and three providers offer keyed endpoints for it. We benchmarked all three on the same call, the same cadence, from the same place. Here is who is fastest." +heroFinding: "Chainstack returns a Robinhood Chain block in about 5 ms at the median, the lowest of any keyed provider on the chain." +author: "OpenChainBench Research" +readingTime: 6 +canonical: "https://openchainbench.com/reports/rpc/2026-08-fastest-robinhood-chain-rpc" +--- + + +- Robinhood Chain (chain ID 4663) is an Arbitrum Orbit L2 for tokenized equity trading, live on mainnet since July 2026 with a gas subsidy running through September 29, 2026. +- Three providers currently expose keyed Robinhood Chain endpoints: Chainstack, Alchemy, and QuickNode. This bench probes all three. +- Chainstack posts the lowest median latency in the cohort at roughly 5 ms (p50, 24h). +- Alchemy follows at ~17 ms, QuickNode at ~71 ms. All three returned a usable block on 100% of probes. +- This is the first and so far only independent public latency benchmark for Robinhood Chain. + + +## Methodology + +Every number here is a live query against the [OpenChainBench Prometheus](https://openchainbench.com/methodology). The harness sends each provider an `eth_getBlockByNumber("latest", false)` request every 60 seconds, with a rotating JSON-RPC id so no edge cache can serve a canned response. Each probe forces a real Robinhood Chain tip. + +Each response is classified as `ok`, `http_err`, `jsonrpc_err`, `stale` (block more than 20 behind the cross-provider tip), or `timeout`. Latency is recorded only for `ok` responses, and the figures below are the p50 over a rolling 24-hour window. Probes run from US-East (Virginia), where our infrastructure sits; that is the reference region for this bench. + +Live page, sparklines, and source: [keyed-rpc-robinhood](/benchmarks/keyed-rpc-robinhood). + +## The Result: Chainstack Leads on Latency + +Three providers offer keyed Robinhood Chain endpoints today, and all three answer reliably. None dropped a probe over the window. So this is a clean speed comparison, and the gap between them is wide. + +Chainstack returns a Robinhood Chain block in about 5 ms at the median, the lowest p50 of any keyed provider on the chain. Its Global Node was the first keyed endpoint live on Robinhood Chain, and at the median it clears requests roughly three times faster than the next provider. + + + +Alchemy lands second at ~17 ms via its edge infrastructure. QuickNode's shared-endpoint fleet sits at ~71 ms. All three cleared 100% of probes, so the ranking is purely about speed, and on a ~100 ms block-time chain built for equity trading, median RPC latency is the number that matters. + +## Why Latency Matters More Here + +Robinhood Chain is a financially sensitive chain: it exists to move tokenized equities, and blocks arrive roughly every 100 ms. A read that lags the tip is a read against a stale price. That makes the median round-trip to your RPC a direct input to how fresh your view of the chain is, whether that is a quote engine, a trading frontend, or any component that polls the head. + +At ~5 ms median, Chainstack leaves the most headroom against that 100 ms cadence of the three benched providers. + +## Which to Use + + +Chainstack is the median leader in this cohort at ~5 ms p50 from US-East. If your priority is the fastest typical read on Robinhood Chain, it is the front-runner today. + + + +Both answer reliably on Robinhood Chain (100% success over the window). Alchemy at ~17 ms and QuickNode at ~71 ms are workable; the trade-off against Chainstack is median latency, not availability. + + +## What to Watch Next + +Robinhood Chain is a new chain, and the keyed-provider field is still small. As more providers stand up keyed endpoints and as regional coverage grows, this bench will track them. The live rankings update every 60 seconds on the [keyed-rpc-robinhood bench](/benchmarks/keyed-rpc-robinhood). + +## Sources and Reproducibility + +- **Live bench:** [keyed-rpc-robinhood](/benchmarks/keyed-rpc-robinhood) +- **Harness source:** [harnesses/rpc-keyed-latency](https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-keyed-latency) +- **License:** CC BY 4.0, reproduce with attribution to OpenChainBench and a link to the canonical URL. +- **Corrections:** file a [GitHub issue](https://github.com/ChainBench/OpenChainBench/issues/new). + +*Figures are the 24h window as of 2026-08-31 and move as new samples land. Check the live page for current numbers.* From 4de0f17e792cc71839470af397766708b66c7dbb Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 31 Aug 2026 19:01:05 +0200 Subject: [PATCH 25/66] fee-compare: signed funding, maker/taker-aware equiv, symmetric coin filter --- src/app/api/fee-compare/route.ts | 332 ++++++++++++++------------ src/components/fee-compare-client.tsx | 34 ++- 2 files changed, 194 insertions(+), 172 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index 79ea186a9..129b0059e 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -84,7 +84,7 @@ let gainsFeeCache: { ts: number; } | null = null; -type RateCacheEntry = { rate: number; note: string; ts: number }; +type RateCacheEntry = { rate: number; makerRate: number; note: string; ts: number }; const rateCache: Partial> = {}; type CarryRates = { @@ -112,7 +112,7 @@ type HlFill = { type HlFundingEvent = { time: number; - delta: { usdc: string }; + delta: { usdc: string; coin?: string }; }; type GainsApiTrade = { @@ -372,7 +372,7 @@ async function fetchGainsFeeRates(): Promise<{ return gainsFeeCache; } -async function fetchHlRate(): Promise<{ rate: number; note: string }> { +async function fetchHlRate(): Promise<{ rate: number; makerRate: number; note: string }> { const cached = rateCache["hyperliquid"]; if (cached && Date.now() - cached.ts < RATE_CACHE_TTL_MS) return cached; const res = await fetch(HL_API, { @@ -381,25 +381,28 @@ async function fetchHlRate(): Promise<{ rate: number; note: string }> { body: JSON.stringify({ type: "userFees", user: "0x0000000000000000000000000000000000000000" }), signal: AbortSignal.timeout(8000), }); - const data = (await res.json()) as { userCrossRate?: string }; + const data = (await res.json()) as { userCrossRate?: string; userAddRate?: string }; const rate = parseFloat(data.userCrossRate ?? String(HL_TAKER_FALLBACK)); - const entry = { rate, note: `${(rate * 10000).toFixed(2)} bps taker (live from HL fee schedule)`, ts: Date.now() }; + // userAddRate = maker (add-liquidity) rate; base tier is ~1.0 bps + const makerRate = parseFloat(data.userAddRate ?? String(rate)); + const entry = { rate, makerRate, note: `${(rate * 10000).toFixed(2)} bps taker (live from HL fee schedule)`, ts: Date.now() }; rateCache["hyperliquid"] = entry; return entry; } -async function fetchParadexRate(): Promise<{ rate: number; note: string }> { +async function fetchParadexRate(): Promise<{ rate: number; makerRate: number; note: string }> { const cached = rateCache["paradex"]; if (cached && Date.now() - cached.ts < RATE_CACHE_TTL_MS) return cached; const res = await fetch("https://api.prod.paradex.trade/v1/markets?market=BTC-USD-PERP", { signal: AbortSignal.timeout(8000), }); const data = (await res.json()) as { - results?: Array<{ fee_config?: { api_fee?: { taker_fee?: { fee?: string } } } }>; + results?: Array<{ fee_config?: { api_fee?: { taker_fee?: { fee?: string }; maker_fee?: { fee?: string } } } }>; }; - const rawRate = data.results?.[0]?.fee_config?.api_fee?.taker_fee?.fee ?? "0.0002"; - const rate = parseFloat(rawRate); - const entry = { rate, note: `${(rate * 10000).toFixed(2)} bps taker (live from Paradex)`, ts: Date.now() }; + const feeCfg = data.results?.[0]?.fee_config?.api_fee; + const rate = parseFloat(feeCfg?.taker_fee?.fee ?? "0.0002"); + const makerRate = parseFloat(feeCfg?.maker_fee?.fee ?? String(rate)); + const entry = { rate, makerRate, note: `${(rate * 10000).toFixed(2)} bps taker (live from Paradex)`, ts: Date.now() }; rateCache["paradex"] = entry; return entry; } @@ -451,19 +454,21 @@ async function fetchParadexCarryRates(): Promise { return result; } -async function fetchEdgeXRate(): Promise<{ rate: number; note: string }> { +async function fetchEdgeXRate(): Promise<{ rate: number; makerRate: number; note: string }> { const cached = rateCache["edgex"]; if (cached && Date.now() - cached.ts < RATE_CACHE_TTL_MS) return cached; const res = await fetch("https://edgex-prod-v2.edgex.exchange/api/v2/public/meta/getMetaData", { signal: AbortSignal.timeout(8000), }); const data = (await res.json()) as { - data?: { contractList?: Array<{ defaultTakerFeeRate?: string | number }> }; + data?: { contractList?: Array<{ defaultTakerFeeRate?: string | number; defaultMakerFeeRate?: string | number }> }; }; const contracts = data.data?.contractList ?? []; const rates = contracts.map((c) => parseFloat(String(c.defaultTakerFeeRate ?? "0"))).filter((r) => r > 0); + const makerRates = contracts.map((c) => parseFloat(String(c.defaultMakerFeeRate ?? "0"))).filter((r) => r > 0); const rate = rates.length > 0 ? rates.reduce((a, b) => a + b, 0) / rates.length : 0.00038; - const entry = { rate, note: `${(rate * 10000).toFixed(2)} bps taker (live from EdgeX)`, ts: Date.now() }; + const makerRate = makerRates.length > 0 ? makerRates.reduce((a, b) => a + b, 0) / makerRates.length : rate; + const entry = { rate, makerRate, note: `${(rate * 10000).toFixed(2)} bps taker (live from EdgeX)`, ts: Date.now() }; rateCache["edgex"] = entry; return entry; } @@ -630,10 +635,14 @@ async function fetchGmxCarryRates(): Promise { if (totalOI === BigInt(0) || fundingFactorRaw === BigInt(0)) return { coin, rate: 0 }; - const imbalance = longsOI > shortsOI ? longsOI - shortsOI : shortsOI - longsOI; - // rate = fundingFactor × (imbalance / totalOI) / 1e30 - const rateScaled = fundingFactorRaw * imbalance / totalOI; - const rate = Number(rateScaled) / 1e30; + // SIGNED imbalance: longs crowded (>0) → longs pay → positive rate. + // Shorts crowded (<0) → shorts pay → negative rate. Keeping the sign lets the + // carry projection charge the correct side (a long is only charged when longs pay). + const signedImbalance = longsOI - shortsOI; + const magnitude = signedImbalance < BigInt(0) ? -signedImbalance : signedImbalance; + // rate = fundingFactor × (|imbalance| / totalOI) / 1e30, re-signed afterwards + const rateScaled = fundingFactorRaw * magnitude / totalOI; + const rate = (Number(rateScaled) / 1e30) * (signedImbalance < BigInt(0) ? -1 : 1); return { coin, rate }; }) @@ -642,8 +651,8 @@ async function fetchGmxCarryRates(): Promise { for (const r of fundingReads) { if (r.status !== "fulfilled" || !r.value) continue; const { coin, rate } = r.value; - // Sanity check: GMX funding should be between 1e-12 and 1e-6 /sec - if (rate > 1e-12 && rate < 1e-6) { + // Sanity check: |GMX funding| should be between 1e-12 and 1e-6 /sec + if (Math.abs(rate) > 1e-12 && Math.abs(rate) < 1e-6) { fundingPerSecPerCoin[coin] = rate; } } @@ -658,7 +667,7 @@ async function fetchGmxCarryRates(): Promise { } } -async function fetchGmxLiveRate(): Promise<{ rate: number; note: string }> { +async function fetchGmxLiveRate(): Promise<{ rate: number; makerRate: number; note: string }> { const cached = rateCache["gmx-v2"]; if (cached && Date.now() - cached.ts < RATE_CACHE_TTL_MS) return cached; // Filter to USDC-collateral only: other tokens have different decimals, @@ -699,34 +708,38 @@ async function fetchGmxLiveRate(): Promise<{ rate: number; note: string }> { totalNotional += notional; } const rate = totalNotional > 0 ? totalFees / totalNotional : 0.0005; - const entry = { rate, note: `${(rate * 10000).toFixed(2)} bps (live avg from recent GMX v2 trades)`, ts: Date.now() }; + // GMX v2 is an AMM-style venue: the position fee is charged regardless of + // whether the order added or removed liquidity, so maker == taker. + const entry = { rate, makerRate: rate, note: `${(rate * 10000).toFixed(2)} bps (live avg from recent GMX v2 trades)`, ts: Date.now() }; rateCache["gmx-v2"] = entry; return entry; } -async function resolveRate(slug: string): Promise<{ rate: number; note: string; rateIsLive: boolean }> { +async function resolveRate(slug: string): Promise<{ rate: number; makerRate: number; note: string; rateIsLive: boolean }> { if (slug === "gains") { + // AMM-style: same position-size fee whether the order adds or removes liquidity. const d = await fetchGainsFeeRates(); - return { rate: d.avgPerSide, note: "Live per-coin taker rate (avg across pairs)", rateIsLive: true }; + return { rate: d.avgPerSide, makerRate: d.avgPerSide, note: "Live per-coin taker rate (avg across pairs)", rateIsLive: true }; } if (slug === "hyperliquid") { - const r = await fetchHlRate().catch(() => ({ rate: HL_TAKER_FALLBACK, note: "3.50 bps taker (HL base tier)" })); + const r = await fetchHlRate().catch(() => ({ rate: HL_TAKER_FALLBACK, makerRate: 0.0001, note: "3.50 bps taker (HL base tier)" })); return { ...r, rateIsLive: true }; } if (slug === "paradex") { - const r = await fetchParadexRate().catch(() => ({ rate: 0.0002, note: "2.00 bps taker (Paradex api-tier)" })); + const r = await fetchParadexRate().catch(() => ({ rate: 0.0002, makerRate: 0.00005, note: "2.00 bps taker (Paradex api-tier)" })); return { ...r, rateIsLive: true }; } if (slug === "edgex") { - const r = await fetchEdgeXRate().catch(() => ({ rate: 0.00038, note: "3.80 bps taker (EdgeX)" })); + const r = await fetchEdgeXRate().catch(() => ({ rate: 0.00038, makerRate: 0.0001, note: "3.80 bps taker (EdgeX)" })); return { ...r, rateIsLive: true }; } if (slug === "gmx-v2") { - const r = await fetchGmxLiveRate().catch(() => ({ rate: 0.0005, note: "5.00 bps taker (GMX v2 fallback)" })); + const r = await fetchGmxLiveRate().catch(() => ({ rate: 0.0005, makerRate: 0.0005, note: "5.00 bps taker (GMX v2 fallback)" })); return { ...r, rateIsLive: true }; } - if (slug === "dydx") return { rate: 0.0005, note: "5.00 bps taker (tier-0, protocol-governed)", rateIsLive: false }; - return { rate: 0.0005, note: "Documented rate", rateIsLive: false }; + // dYdX v4 tier-0: 5.0 bps taker / 1.0 bps maker (protocol-governed schedule) + if (slug === "dydx") return { rate: 0.0005, makerRate: 0.0001, note: "5.00 bps taker (tier-0, protocol-governed)", rateIsLive: false }; + return { rate: 0.0005, makerRate: 0.0005, note: "Documented rate", rateIsLive: false }; } type HlOpenPos = { @@ -1256,21 +1269,6 @@ function reconstructGmxPositions(rawTrades: RawGmxTrade[], cutoffMs: number): Po return slices; } -// Estimate GMX borrow fees for a set of position slices. -function estimateGmxBorrowFees( - positions: PositionSlice[], - borrowPerSecPerCoin: Record -): number { - const fallbackRate = borrowPerSecPerCoin["BTC"] ?? 1.4e-8; - let total = 0; - for (const pos of positions) { - const rate = borrowPerSecPerCoin[pos.coin] ?? fallbackRate; - const durationSec = Math.max(0, (pos.closeMs - pos.openMs) / 1000); - total += pos.notionalUsd * rate * durationSec; - } - return total; -} - // Fetch HL 8h funding rate history for a set of coins over a period. // Paginates automatically: the HL API returns at most 500 entries per request. // At 3 entries/day, 500 covers ~167 days. Windows >167d need multiple pages. @@ -1352,7 +1350,9 @@ function estimateGainsBorrowFees( // Estimate Gains funding fees for a set of position slices. // Uses the current (last known) per-second funding rate as a proxy for the period. -// Rate is absolute (direction already irrelevant for cost estimation). +// SIGNED: a long pays when rate>0 and receives when rate<0 (and vice-versa for shorts). +// Keeping the sign is what makes the projection apple-to-apple with HL's realized +// funding, which also credits the wallet when it was on the paid-to side. function estimateGainsFundingFees( positions: PositionSlice[], fundingPerSecPerCoin: Record @@ -1362,9 +1362,9 @@ function estimateGainsFundingFees( const rate = fundingPerSecPerCoin[pos.coin]; if (!rate) continue; const durationSec = Math.max(0, (pos.closeMs - pos.openMs) / 1000); - // positive rate = longs pay; negative rate = shorts pay - const effectiveRate = pos.isLong ? Math.max(0, rate) : Math.max(0, -rate); - total += pos.notionalUsd * effectiveRate * durationSec; + // positive rate = longs pay shorts; a long's cost is +rate, a short's is -rate + const signedRate = pos.isLong ? rate : -rate; + total += pos.notionalUsd * signedRate * durationSec; } return total; } @@ -1379,9 +1379,10 @@ function estimateCarryFees( const durationSec = Math.max(0, (pos.closeMs - pos.openMs) / 1000); borrowFees += pos.notionalUsd * (rates.borrowPerSecPerCoin[pos.coin] ?? 0) * durationSec; const fundingRate = rates.fundingPerSecPerCoin[pos.coin] ?? 0; - // positive rate = longs pay; negative rate = shorts pay - const fundingCost = pos.isLong ? Math.max(0, fundingRate) : Math.max(0, -fundingRate); - fundingFees += pos.notionalUsd * fundingCost * durationSec; + // SIGNED: positive rate = longs pay shorts. A long's cost is +rate, a short's is -rate. + // Signed carry lets a wallet on the receiving side show a funding credit. + const signedRate = pos.isLong ? fundingRate : -fundingRate; + fundingFees += pos.notionalUsd * signedRate * durationSec; } return { borrowFees, fundingFees }; } @@ -1423,6 +1424,91 @@ function walletStats(slug: string, w: AnyWallet, otherSlug?: string): { notional return null; } +type GainsRateData = { + perSide: Record; + avgPerSide: number; + borrowPerSecPerCoin: Record; + avgBorrowPerSec: number; + fundingPerSecPerCoin: Record; +}; + +// Maker/taker-aware taker-equivalent for a set of HL fills projected onto an +// order-book venue: a fill that added liquidity on HL (crossed=false) is assumed +// to add liquidity on the target too, so it gets the maker rate. On AMM targets +// pass makerRate === takerRate and every fill is charged the same. +function hlMakerAwareEquiv(fills: HlFill[], takerRate: number, makerRate: number): number { + let sum = 0; + for (const f of fills) { + const notional = parseFloat(f.px) * parseFloat(f.sz); + sum += notional * (f.crossed ? takerRate : makerRate); + } + return sum; +} + +// Shared HL ↔ Gains projection. Restricts to coins Gains actually lists (apple to +// apple), keeps HL funding signed, and projects Gains carry from reconstructed HL +// positions. Returns null when the wallet has no HL fills on Gains-listed coins. +function computeHlGainsSim( + hlFills: HlFill[], + hlFundingEvents: HlFundingEvent[], + hlOpenPositions: HlOpenPos[], + cutoffMs: number, + gainsData: GainsRateData +): { sim: SimResult; hlNetBps: number; gainsEffBps: number } | null { + const inGains = (coin: string) => gainsData.perSide[coin] !== undefined; + const recent = hlFills.filter((f) => f.time >= cutoffMs && inGains(f.coin)); + if (recent.length === 0) return null; + + let takerEquiv = 0; + let notional = 0; + let hlFees = 0; + for (const fill of recent) { + const n = parseFloat(fill.px) * parseFloat(fill.sz); + // Gains is AMM-style: same fee regardless of maker/taker, so per-coin rate applies to all. + const coinRate = gainsData.perSide[fill.coin] ?? gainsData.avgPerSide; + takerEquiv += n * coinRate; + notional += n; + hlFees += parseFloat(fill.fee); + } + if (notional <= 0) return null; + + // HL realized funding restricted to Gains-comparable coins (delta may omit coin → keep it). + const hlFunding = hlFundingEvents + .filter((f) => f.time >= cutoffMs && (f.delta.coin === undefined || inGains(f.delta.coin))) + .reduce((s, f) => s + parseFloat(f.delta?.usdc ?? "0"), 0); + const hlNet = hlFees - hlFunding; + + // Project Gains carry from HL positions on comparable coins only. + const positions = augmentWithHlOpenPositions( + reconstructHlPositions(hlFills, cutoffMs), + hlOpenPositions, + cutoffMs + ).filter((p) => inGains(p.coin)); + const gainsBorrow = estimateGainsBorrowFees(positions, gainsData.borrowPerSecPerCoin, gainsData.avgBorrowPerSec); + const gainsFunding = estimateGainsFundingFees(positions, gainsData.fundingPerSecPerCoin); + const equiv = takerEquiv + gainsBorrow + gainsFunding; + + return { + sim: { + notionalUsed: notional, + feesActual: hlNet, + equivFees: equiv, + saved: equiv - hlNet, + multiple: hlNet > 0 ? equiv / hlNet : null, + fundingUsd: hlFunding, + projectedCarry: { + takerFees: takerEquiv, + borrowFees: gainsBorrow, + fundingFees: gainsFunding, + borrowProjected: gainsBorrow > 0.01, + fundingProjected: Math.abs(gainsFunding) > 0.01, + }, + }, + hlNetBps: (hlNet / notional) * 10000, + gainsEffBps: (equiv / notional) * 10000, + }; +} + // ────────────────────────────────────────────────────────────────────── // Route // ────────────────────────────────────────────────────────────────────── @@ -1464,8 +1550,8 @@ export async function GET(req: Request) { try { const [ - { rate: rateA, note: noteA, rateIsLive: rateIsLiveA }, - { rate: rateB, note: noteB, rateIsLive: rateIsLiveB }, + { rate: rateA, makerRate: makerRateA, note: noteA, rateIsLive: rateIsLiveA }, + { rate: rateB, makerRate: makerRateB, note: noteB, rateIsLive: rateIsLiveB }, gainsData, dydxCarryData, paradexCarryData, @@ -1596,18 +1682,22 @@ export async function GET(req: Request) { 0 ); walletData = buildHlWalletData(recentFills, fundingTotal); - // Annotate each fill with the equivalent fee on the other venue + // Annotate each fill with the equivalent fee on the other venue. const otherSlug = slug === venueA ? venueB : venueA; const otherRate = slug === venueA ? rateB : rateA; + const otherMakerRate = slug === venueA ? makerRateB : makerRateA; const hlW = walletData as HlWalletData; - hlW.recentFills = hlW.recentFills.map((fill) => ({ - ...fill, - equivFee: otherSlug === "gains" - ? fill.notional * (gainsData.perSide[fill.coin] ?? gainsData.avgPerSide) - : fill.notional * otherRate, - })); + hlW.recentFills = hlW.recentFills.map((fill) => { + if (otherSlug === "gains") { + // Only comparable when Gains lists the coin; else leave undefined (n/a). + const coinRate = gainsData.perSide[fill.coin]; + return { ...fill, equivFee: coinRate !== undefined ? fill.notional * coinRate : undefined }; + } + // Order-book / AMM target: preserve execution style (maker fills → maker rate). + const targetRate = fill.isTaker ? otherRate : otherMakerRate; + return { ...fill, equivFee: fill.notional * targetRate }; + }); } else if (fetchEvmWallet && slug === "gains") { - const CLOSE_ACTIONS = new Set(["TradeClosedMarket", "TradeClosedTP", "TradeClosedSL", "TradeClosedLIQ"]); const usdcTrades = gainsTradesData.filter((t) => t.collateralIndex === 3); const otherSlug = slug === venueA ? venueB : venueA; const otherRate = slug === venueA ? rateB : rateA; @@ -1702,60 +1792,21 @@ export async function GET(req: Request) { // aToBSim: venueA actual fills vs simulated venueB cost (with carry projection) if (venueAResult.wallet !== null) { if (venueA === "hyperliquid" && venueB === "gains") { - // Per-coin Gains taker rates on HL fills + estimated Gains borrow - const hlW = venueAResult.wallet as HlWalletData; - if (hlW.fills > 0) { - let takerEquiv = 0, aNotional = 0, aFees = 0; - for (const fill of hlFillsData.filter((f) => f.time >= cutoffMs)) { - const notional = parseFloat(fill.px) * parseFloat(fill.sz); - const fee = parseFloat(fill.fee); - const coinRate = gainsData.perSide[fill.coin] ?? gainsData.avgPerSide; - takerEquiv += notional * coinRate; - aNotional += notional; - aFees += fee; - } - const aFunding = hlW.fundingUsd; - const aNetCost = aFees - aFunding; - - // Estimate Gains carry (borrow + funding) by reconstructing HL positions. - // augmentWithHlOpenPositions fills in positions whose open fill is older than the - // 2000-fill API cap — they still generate real HL funding but are invisible to - // fill-only reconstruction. - const hlPositions = augmentWithHlOpenPositions( - reconstructHlPositions(hlFillsData, cutoffMs), - hlOpenPositions, - cutoffMs - ); - const gainsBorrow = estimateGainsBorrowFees(hlPositions, gainsData.borrowPerSecPerCoin, gainsData.avgBorrowPerSec); - const gainsFunding = estimateGainsFundingFees(hlPositions, gainsData.fundingPerSecPerCoin); - const bEquiv = takerEquiv + gainsBorrow + gainsFunding; - - comparison.aToBSim = { - notionalUsed: aNotional, - feesActual: aNetCost, - equivFees: bEquiv, - saved: bEquiv - aNetCost, - multiple: aNetCost > 0 ? bEquiv / aNetCost : null, - fundingUsd: aFunding, - projectedCarry: { - takerFees: takerEquiv, - borrowFees: gainsBorrow, - fundingFees: gainsFunding, - borrowProjected: gainsBorrow > 0.01, - fundingProjected: gainsFunding > 0.01, - }, - }; - if (aNotional > 0) { - venueAResult.effectiveRateBps = (aNetCost / aNotional) * 10000; - venueAResult.effectiveRateNote = `${((aNetCost / aNotional) * 10000).toFixed(2)} bps net (fees + funding)`; - venueBResult.effectiveRateBps = (bEquiv / aNotional) * 10000; - venueBResult.effectiveRateNote = `${((bEquiv / aNotional) * 10000).toFixed(2)} bps effective (your coins)`; - } + const r = computeHlGainsSim(hlFillsData, hlFundingData, hlOpenPositions, cutoffMs, gainsData); + if (r) { + comparison.aToBSim = r.sim; + venueAResult.effectiveRateBps = r.hlNetBps; + venueAResult.effectiveRateNote = `${r.hlNetBps.toFixed(2)} bps net (fees + funding)`; + venueBResult.effectiveRateBps = r.gainsEffBps; + venueBResult.effectiveRateNote = `${r.gainsEffBps.toFixed(2)} bps effective (your coins)`; } } else { const stats = walletStats(venueA, venueAResult.wallet, venueB); if (stats) { - let equivFees = stats.notional * rateB; + // HL source: preserve maker/taker style per fill; other sources are AMM (single fee). + let equivFees = venueA === "hyperliquid" + ? hlMakerAwareEquiv(hlFillsData.filter((f) => f.time >= cutoffMs), rateB, makerRateB) + : stats.notional * rateB; let projectedCarry: SimResult["projectedCarry"]; // Reconstruct positions from venueA for carry projection @@ -1794,7 +1845,6 @@ export async function GET(req: Request) { } // HL→GMX: use wallet's own GMX history as carry proxy - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const gmxForA = gmxWalletData as GmxWalletData | null; if (venueA === "hyperliquid" && venueB === "gmx-v2" && gmxForA !== null && gmxForA.notionalUsd > 0) { const takerFees = equivFees; @@ -1849,56 +1899,21 @@ export async function GET(req: Request) { // bToASim: venueB actual fills vs simulated venueA cost (with carry projection) if (venueBResult.wallet !== null) { if (venueB === "hyperliquid" && venueA === "gains") { - // Per-coin Gains taker rates on HL fills + estimated Gains borrow - const hlW = venueBResult.wallet as HlWalletData; - if (hlW.fills > 0) { - let takerEquiv = 0, bNotional = 0, bFees = 0; - for (const fill of hlFillsData.filter((f) => f.time >= cutoffMs)) { - const notional = parseFloat(fill.px) * parseFloat(fill.sz); - const fee = parseFloat(fill.fee); - const coinRate = gainsData.perSide[fill.coin] ?? gainsData.avgPerSide; - takerEquiv += notional * coinRate; - bNotional += notional; - bFees += fee; - } - const bFunding = hlW.fundingUsd; - const bNetCost = bFees - bFunding; - - const hlPositions = augmentWithHlOpenPositions( - reconstructHlPositions(hlFillsData, cutoffMs), - hlOpenPositions, - cutoffMs - ); - const gainsBorrow = estimateGainsBorrowFees(hlPositions, gainsData.borrowPerSecPerCoin, gainsData.avgBorrowPerSec); - const gainsFunding = estimateGainsFundingFees(hlPositions, gainsData.fundingPerSecPerCoin); - const aEquiv = takerEquiv + gainsBorrow + gainsFunding; - - comparison.bToASim = { - notionalUsed: bNotional, - feesActual: bNetCost, - equivFees: aEquiv, - saved: aEquiv - bNetCost, - multiple: bNetCost > 0 ? aEquiv / bNetCost : null, - fundingUsd: bFunding, - projectedCarry: { - takerFees: takerEquiv, - borrowFees: gainsBorrow, - fundingFees: gainsFunding, - borrowProjected: gainsBorrow > 0.01, - fundingProjected: gainsFunding > 0.01, - }, - }; - if (bNotional > 0) { - venueBResult.effectiveRateBps = (bNetCost / bNotional) * 10000; - venueBResult.effectiveRateNote = `${((bNetCost / bNotional) * 10000).toFixed(2)} bps net (fees + funding)`; - venueAResult.effectiveRateBps = (aEquiv / bNotional) * 10000; - venueAResult.effectiveRateNote = `${((aEquiv / bNotional) * 10000).toFixed(2)} bps effective (your coins)`; - } + const r = computeHlGainsSim(hlFillsData, hlFundingData, hlOpenPositions, cutoffMs, gainsData); + if (r) { + comparison.bToASim = r.sim; + venueBResult.effectiveRateBps = r.hlNetBps; + venueBResult.effectiveRateNote = `${r.hlNetBps.toFixed(2)} bps net (fees + funding)`; + venueAResult.effectiveRateBps = r.gainsEffBps; + venueAResult.effectiveRateNote = `${r.gainsEffBps.toFixed(2)} bps effective (your coins)`; } } else { const stats = walletStats(venueB, venueBResult.wallet, venueA); if (stats) { - let equivFees = stats.notional * rateA; + // HL source: preserve maker/taker style per fill; other sources are AMM (single fee). + let equivFees = venueB === "hyperliquid" + ? hlMakerAwareEquiv(hlFillsData.filter((f) => f.time >= cutoffMs), rateA, makerRateA) + : stats.notional * rateA; let projectedCarry: SimResult["projectedCarry"]; // Reconstruct positions from venueB @@ -1937,7 +1952,6 @@ export async function GET(req: Request) { } // HL→GMX (venueB=HL, venueA=GMX) - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const gmxForB = gmxWalletData as GmxWalletData | null; if (venueB === "hyperliquid" && venueA === "gmx-v2" && gmxForB !== null && gmxForB.notionalUsd > 0) { const takerFees = equivFees; diff --git a/src/components/fee-compare-client.tsx b/src/components/fee-compare-client.tsx index 2cf2b04c2..e50b624e8 100644 --- a/src/components/fee-compare-client.tsx +++ b/src/components/fee-compare-client.tsx @@ -913,7 +913,7 @@ function WalletSummaryCard({ result }: { result: FeeCompareResult }) {

Wallet analysis

- Net cost including carry (funding + borrowing) vs taker-rate projection on the other venue + Net cost including carry (funding + borrowing) vs a maker/taker-aware projection on the other venue, matched on the coins both venues list

@@ -1085,19 +1085,27 @@ function HlTradeTable({ {fmtUsd(f.hlFee)} - {hasEquiv && f.equivFee !== undefined && ( - f.hlFee ? "text-red-400" : "text-ink-faint" - }`}> - {fmtUsd(f.equivFee)} - + {hasEquiv && ( + f.equivFee !== undefined ? ( + f.hlFee ? "text-red-400" : "text-ink-faint" + }`}> + {fmtUsd(f.equivFee)} + + ) : ( + n/a + ) )} - {hasEquiv && diff !== undefined && ( - 0.001 ? "text-emerald-500" : diff < -0.001 ? "text-red-400" : "text-ink-faint" - }`}> - {diff > 0.001 ? `+${fmtUsd(diff)}` : diff < -0.001 ? fmtUsd(diff) : "—"} - + {hasEquiv && ( + diff !== undefined ? ( + 0.001 ? "text-emerald-500" : diff < -0.001 ? "text-red-400" : "text-ink-faint" + }`}> + {diff > 0.001 ? `+${fmtUsd(diff)}` : diff < -0.001 ? fmtUsd(diff) : "—"} + + ) : ( + — + ) )} {!hasEquiv && ( From a2790ff309796867d72431f9cced30b6fa432452 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 31 Aug 2026 19:16:54 +0200 Subject: [PATCH 26/66] report: retitle Best Robinhood Chain RPC, scope claims to 24h median --- .../2026-08-fastest-robinhood-chain-rpc.mdx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/content/reports/rpc/2026-08-fastest-robinhood-chain-rpc.mdx b/src/content/reports/rpc/2026-08-fastest-robinhood-chain-rpc.mdx index b3727fff8..ac0b0f9f3 100644 --- a/src/content/reports/rpc/2026-08-fastest-robinhood-chain-rpc.mdx +++ b/src/content/reports/rpc/2026-08-fastest-robinhood-chain-rpc.mdx @@ -1,11 +1,11 @@ --- -title: "Fastest Robinhood Chain RPC in 2026: Chainstack vs Alchemy vs QuickNode" +title: "Best Robinhood Chain RPC in 2026: Chainstack vs Alchemy vs QuickNode" category: "rpc" slug: "2026-08-fastest-robinhood-chain-rpc" publishedAt: "2026-08-31" period: "August 2026" -summary: "Robinhood Chain is live, and three providers offer keyed endpoints for it. We benchmarked all three on the same call, the same cadence, from the same place. Here is who is fastest." -heroFinding: "Chainstack returns a Robinhood Chain block in about 5 ms at the median, the lowest of any keyed provider on the chain." +summary: "Robinhood Chain is live, and three providers offer keyed endpoints for it. We benchmarked all three on the same call, the same cadence, over a rolling 24-hour window. Here is which one delivers the fastest median read." +heroFinding: "Chainstack posts the lowest median latency on Robinhood Chain, roughly 7 ms p50 over 24 hours, the best of any keyed provider on the chain." author: "OpenChainBench Research" readingTime: 6 canonical: "https://openchainbench.com/reports/rpc/2026-08-fastest-robinhood-chain-rpc" @@ -14,39 +14,39 @@ canonical: "https://openchainbench.com/reports/rpc/2026-08-fastest-robinhood-cha - Robinhood Chain (chain ID 4663) is an Arbitrum Orbit L2 for tokenized equity trading, live on mainnet since July 2026 with a gas subsidy running through September 29, 2026. - Three providers currently expose keyed Robinhood Chain endpoints: Chainstack, Alchemy, and QuickNode. This bench probes all three. -- Chainstack posts the lowest median latency in the cohort at roughly 5 ms (p50, 24h). +- Chainstack posts the lowest median latency in the cohort, roughly 7 ms (p50, 24h). - Alchemy follows at ~17 ms, QuickNode at ~71 ms. All three returned a usable block on 100% of probes. -- This is the first and so far only independent public latency benchmark for Robinhood Chain. +- This is the first independent public latency benchmark for Robinhood Chain. ## Methodology Every number here is a live query against the [OpenChainBench Prometheus](https://openchainbench.com/methodology). The harness sends each provider an `eth_getBlockByNumber("latest", false)` request every 60 seconds, with a rotating JSON-RPC id so no edge cache can serve a canned response. Each probe forces a real Robinhood Chain tip. -Each response is classified as `ok`, `http_err`, `jsonrpc_err`, `stale` (block more than 20 behind the cross-provider tip), or `timeout`. Latency is recorded only for `ok` responses, and the figures below are the p50 over a rolling 24-hour window. Probes run from US-East (Virginia), where our infrastructure sits; that is the reference region for this bench. +Each response is classified as ok, http_err, jsonrpc_err, stale (block more than 20 behind the cross-provider tip), or timeout. Latency is recorded only for ok responses, and the figures below are the p50 over a rolling 24-hour window. Probes run from US-East (Virginia), where our infrastructure sits; that is the reference region for this bench. Live page, sparklines, and source: [keyed-rpc-robinhood](/benchmarks/keyed-rpc-robinhood). -## The Result: Chainstack Leads on Latency +## The Result: Chainstack Leads on Median Latency -Three providers offer keyed Robinhood Chain endpoints today, and all three answer reliably. None dropped a probe over the window. So this is a clean speed comparison, and the gap between them is wide. +Three providers offer keyed Robinhood Chain endpoints today, and all three answer reliably. None dropped a probe over the window. So this is a clean speed comparison, and on the median the gap is wide. -Chainstack returns a Robinhood Chain block in about 5 ms at the median, the lowest p50 of any keyed provider on the chain. Its Global Node was the first keyed endpoint live on Robinhood Chain, and at the median it clears requests roughly three times faster than the next provider. +Chainstack returns a Robinhood Chain block in roughly 7 ms at the median over 24 hours, the lowest p50 of any keyed provider on the chain. At the median it clears requests more than twice as fast as the next provider in the cohort. -Alchemy lands second at ~17 ms via its edge infrastructure. QuickNode's shared-endpoint fleet sits at ~71 ms. All three cleared 100% of probes, so the ranking is purely about speed, and on a ~100 ms block-time chain built for equity trading, median RPC latency is the number that matters. +Alchemy lands second at ~17 ms via its edge infrastructure. QuickNode's shared-endpoint fleet sits at ~71 ms. All three cleared 100% of probes over the window, so on availability the field is even; the separation is in how fast the typical read comes back. -## Why Latency Matters More Here +## Why Median Latency Matters Here -Robinhood Chain is a financially sensitive chain: it exists to move tokenized equities, and blocks arrive roughly every 100 ms. A read that lags the tip is a read against a stale price. That makes the median round-trip to your RPC a direct input to how fresh your view of the chain is, whether that is a quote engine, a trading frontend, or any component that polls the head. +Robinhood Chain is a financially sensitive chain: it exists to move tokenized equities, and blocks arrive roughly every 100 ms. A read that lags the tip is a read against a stale price. For the calls an application makes most, the median round-trip is what most of those reads actually experience, so it is a direct input to how fresh your view of the chain is, whether that is a quote engine, a trading frontend, or any component that polls the head. -At ~5 ms median, Chainstack leaves the most headroom against that 100 ms cadence of the three benched providers. +On the 24-hour median, Chainstack leaves the most headroom against that 100 ms cadence of the three benched providers. ## Which to Use -Chainstack is the median leader in this cohort at ~5 ms p50 from US-East. If your priority is the fastest typical read on Robinhood Chain, it is the front-runner today. +Chainstack is the median leader in this cohort, roughly 7 ms p50 over 24h from US-East. If your priority is the fastest typical read on Robinhood Chain, it is the front-runner today. As with any single endpoint, pair it with a fallback so a provider-side incident never leaves you without a tip. @@ -55,7 +55,7 @@ Both answer reliably on Robinhood Chain (100% success over the window). Alchemy ## What to Watch Next -Robinhood Chain is a new chain, and the keyed-provider field is still small. As more providers stand up keyed endpoints and as regional coverage grows, this bench will track them. The live rankings update every 60 seconds on the [keyed-rpc-robinhood bench](/benchmarks/keyed-rpc-robinhood). +Robinhood Chain is a new chain, and the keyed-provider field is still small. As more providers stand up keyed endpoints and as regional coverage grows, this bench will track them. Latency on a young chain moves day to day, so the live rankings, updated every 60 seconds on the [keyed-rpc-robinhood bench](/benchmarks/keyed-rpc-robinhood), are always the current source of truth. ## Sources and Reproducibility @@ -64,4 +64,4 @@ Robinhood Chain is a new chain, and the keyed-provider field is still small. As - **License:** CC BY 4.0, reproduce with attribution to OpenChainBench and a link to the canonical URL. - **Corrections:** file a [GitHub issue](https://github.com/ChainBench/OpenChainBench/issues/new). -*Figures are the 24h window as of 2026-08-31 and move as new samples land. Check the live page for current numbers.* +*Figures are the 24h median as of 2026-08-31 and move as new samples land. Check the live page for current numbers.* From 2caf47a258b36a0cf3f0e9cc6a4bbf2a97394c1a Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:23:26 +0200 Subject: [PATCH 27/66] fix(reports): use per-report OG card instead of site-wide fallback --- src/app/reports/[category]/[slug]/page.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app/reports/[category]/[slug]/page.tsx b/src/app/reports/[category]/[slug]/page.tsx index 9baf08937..7e2677ed1 100644 --- a/src/app/reports/[category]/[slug]/page.tsx +++ b/src/app/reports/[category]/[slug]/page.tsx @@ -31,7 +31,13 @@ export async function generateMetadata({ params }: Props): Promise { if (!report) return {}; const canonical = `${SITE.url}/reports/${category}/${slug}`; - const ogImage = report.ogImage ?? `${SITE.url}/opengraph-image`; + // Use the per-report OG card (dynamic opengraph-image.tsx in this + // segment) as the default, NOT the site-wide /opengraph-image. Setting + // openGraph.images explicitly here shadows Next's file convention, so + // without this the bespoke titled card was never emitted and every + // report shared the generic site thumbnail on X / LinkedIn / SERP. + const ogImage = + report.ogImage ?? `${SITE.url}/reports/${category}/${slug}/opengraph-image`; const social = `${report.title} · OpenChainBench`; return { @@ -264,7 +270,9 @@ function reportJsonLd(report: ReturnType) { description: report.summary, datePublished: new Date(report.publishedAt).toISOString(), dateModified: new Date(report.publishedAt).toISOString(), - image: report.ogImage ?? `${SITE.url}/opengraph-image`, + image: + report.ogImage ?? + `${SITE.url}/reports/${report.categorySlug}/${report.slug}/opengraph-image`, author: { "@id": PERSON_ID }, publisher: { "@id": `${SITE.url}/#org` }, inLanguage: "en", From 7fa9eb8c8ca249019ba90991e1422b208cde1020 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:27:42 +0200 Subject: [PATCH 28/66] feat(rpc): link keyed-rpc benches into the /rpc cluster (both directions) --- src/app/benchmarks/[slug]/page.tsx | 3 ++- src/app/rpc/page.tsx | 33 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/app/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx index dd95803d8..1c6037470 100644 --- a/src/app/benchmarks/[slug]/page.tsx +++ b/src/app/benchmarks/[slug]/page.tsx @@ -586,7 +586,8 @@ export default async function BenchmarkPage({
)} {(benchmark.slug.endsWith("-rpc") || - benchmark.slug === "rpc-capabilities") && ( + benchmark.slug === "rpc-capabilities" || + benchmark.slug.startsWith("keyed-rpc-")) && (
s.slug.endsWith("-rpc") && !NON_CHAIN_RPC_SLUGS.has(s.slug)) .sort((a, b) => a.slug.localeCompare(b.slug)); + // Keyed / API-key RPC benches (e.g. keyed-rpc-robinhood). These do NOT + // belong in the no-key chain matrix above (different tier, and the + // slug->chain derivation `replace(/-rpc$/,'')` doesn't apply), so we + // surface them in a dedicated aside. Without this they were orphaned + // from the RPC cluster with no inbound link from the hub. + const keyedRpcSpecs = specs + .filter((s) => s.slug.startsWith("keyed-rpc-")) + .sort((a, b) => a.slug.localeCompare(b.slug)); const breadcrumbLd = { "@context": "https://schema.org", @@ -150,6 +158,31 @@ export default async function RpcHubPage() {
+ {keyedRpcSpecs.length > 0 && ( +
+

+ Keyed & premium endpoints +

+

+ The matrix above ranks free, no-key public RPCs. Newer chains + served only through API-key endpoints get their own keyed + benchmark: +

+
    + {keyedRpcSpecs.map((s) => ( +
  • + + {s.title} + +
  • + ))} +
+
+ )} + {snapshot ? ( <>
From ee79b599bc59377714437a53d61df614a39afa7f Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:33:20 +0200 Subject: [PATCH 29/66] feat(bench-243): add Singapore probe region to keyed-rpc-robinhood --- benchmarks/keyed-rpc-robinhood.yml | 76 +++++++++++++++++------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/benchmarks/keyed-rpc-robinhood.yml b/benchmarks/keyed-rpc-robinhood.yml index 09e914e11..b92414e9e 100644 --- a/benchmarks/keyed-rpc-robinhood.yml +++ b/benchmarks/keyed-rpc-robinhood.yml @@ -18,39 +18,49 @@ seo_intro: | subsidy running through September 2026. It is a financially-sensitive chain with low tolerance for RPC latency. This bench probes Chainstack, Alchemy, and QuickNode on their keyed - Robinhood Chain endpoints every 60 seconds from US-East (Virginia). - Chainstack leads from US-East. This is the only independently-run public + Robinhood Chain endpoints every 60 seconds from two probe regions + (US-East and Singapore). Chainstack posts the lowest median latency. This is the only independently-run public latency benchmark for Robinhood Chain. abstract: | We measure the round-trip latency of eth_getBlockByNumber against the keyed endpoints of Chainstack, Alchemy, and QuickNode on Robinhood Chain. The probe uses a rotating JSON-RPC id to defeat edge caches, fires every - 60 seconds from US-East (Virginia), and classifies each response as ok, + 60 seconds from each probe region, and classifies each response as ok, http_err, jsonrpc_err, stale, or timeout. Only ok responses contribute to the latency distribution. methodology: - - "Cadence: every 60 seconds per provider from US-East (Virginia). Robinhood Chain runs on the Arbitrum Orbit stack with ~100ms block times; the 60s probe cadence captures 600 blocks per hour." + - "Cadence: every 60 seconds per provider from each probe region (US-East Virginia; Singapore). Headline numbers average both regions; the region tabs re-scope every figure to one origin. Robinhood Chain runs on the Arbitrum Orbit stack with ~100ms block times; the 60s probe cadence captures 600 blocks per hour." - "Payload: eth_getBlockByNumber('latest', false) with a rotating JSON-RPC id. Non-cacheable by design: the rotating id defeats body-keyed edge caches and forces the provider to serve a real Robinhood Chain tip." - "Authentication: Chainstack Global Node key (node-scoped, dedicated to Robinhood Chain; key in Railway env, not in repo). Alchemy uses its standard multi-chain key. QuickNode uses its shared-endpoint key for Robinhood Chain. All keys in Railway env, not in repo." - "Call-result classification: ok (HTTP 200 with usable block number), http_err, jsonrpc_err (HTTP 200 with error body), stale (block more than 20 behind the cross-provider tip), timeout. Latency is recorded only for ok responses." - "Cohort: Chainstack, Alchemy, and QuickNode. Infura and Ankr do not currently list Robinhood Chain in their supported networks." findings: - - "{{name:chainstack}} leads at {{p50:chainstack}} (p50, 24h) from US-East. Its Global Node, launched first on Robinhood Chain, delivers the lowest round-trip latency in this benchmark." - - "{{name:alchemy}} records {{p50:alchemy}} from US-East via its edge infrastructure." + - "{{name:chainstack}} leads at {{p50:chainstack}} (p50, 24h). Its Global Node, launched first on Robinhood Chain, delivers the lowest round-trip latency in this benchmark." + - "{{name:alchemy}} records {{p50:alchemy}} via its edge infrastructure." - "{{name:quicknode}} delivers {{p50:quicknode}} via its shared-endpoint fleet." +# Per-cell (region) ranking matrix for scoped badge claims. Chain is +# fixed for the whole bench, so cells key on provider and region. +rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="robinhood",tier="keyed"}) + +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: sgp, label: Singapore } + faq: - q: "Which Robinhood Chain RPC provider is fastest?" - a: "Per the live data above: {{name:chainstack}} at {{p50:chainstack}} p50 (24h, from US-East). It is the lowest-latency keyed provider for Robinhood Chain in this benchmark." + a: "Per the live data above: {{name:chainstack}} at {{p50:chainstack}} p50 (24h). It is the lowest-latency keyed provider for Robinhood Chain in this benchmark." - q: "What is Robinhood Chain?" a: "Robinhood Chain (chain ID 4663) is an Arbitrum Orbit L2 built by Robinhood for tokenized equity trading. It launched on mainnet in July 2026 with ~100ms block times and a gas fee subsidy running through September 29, 2026. It supports EVM-compatible tooling and standard eth_ JSON-RPC methods." - q: "Why is this the only Robinhood Chain latency benchmark?" a: "Robinhood Chain is a new chain. OpenChainBench added it within days of Chainstack, Alchemy, and QuickNode support going live, making this the first and so far only independent latency measurement. Infura and Ankr do not currently list Robinhood Chain in their supported networks." - - q: "Why only US-East?" - a: "The probe runs from US-East (Virginia) where the Railway infrastructure is located. Additional probe regions may be added as the chain matures and more regional endpoints become available." + - q: "Which regions does this benchmark probe from?" + a: "Two origins: US-East (Virginia) and Singapore. The headline figures average both; the region tabs at the top of the page re-scope every number to a single origin. Pick the tab closest to where your requests originate." source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-keyed-latency @@ -62,38 +72,38 @@ providers: - slug: chainstack name: Chainstack tag: Global Node, keyed Robinhood Chain endpoint, plan disclosed - formula: "p50 over 24h of round-trip latency (ms) for eth_getBlockByNumber probed every 60s from US-East on Chainstack's keyed Robinhood Chain Global Node." + formula: "p50 over 24h of round-trip latency (ms) for eth_getBlockByNumber probed every 60s per region on Chainstack's keyed Robinhood Chain Global Node." queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) / sum(ocb:rpc_call:rate_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}[1h])) + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="chainstack",chain="robinhood",tier="keyed"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="chainstack",chain="robinhood",tier="keyed"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="chainstack",chain="robinhood",tier="keyed"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="chainstack",chain="robinhood",tier="keyed"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="chainstack",chain="robinhood",tier="keyed"}) / sum(ocb:rpc_call:rate_24h{provider="chainstack",chain="robinhood",tier="keyed"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="chainstack",chain="robinhood",tier="keyed"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="chainstack",chain="robinhood",tier="keyed"}[1h])) - slug: quicknode name: QuickNode tag: Shared endpoint fleet, keyed Robinhood Chain endpoint, plan disclosed - formula: "p50 over 24h of round-trip latency (ms) for eth_getBlockByNumber probed every 60s from US-East on QuickNode's keyed Robinhood Chain endpoint." + formula: "p50 over 24h of round-trip latency (ms) for eth_getBlockByNumber probed every 60s per region on QuickNode's keyed Robinhood Chain endpoint." queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) / sum(ocb:rpc_call:rate_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}[1h])) + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="quicknode",chain="robinhood",tier="keyed"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="quicknode",chain="robinhood",tier="keyed"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="quicknode",chain="robinhood",tier="keyed"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="quicknode",chain="robinhood",tier="keyed"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="quicknode",chain="robinhood",tier="keyed"}) / sum(ocb:rpc_call:rate_24h{provider="quicknode",chain="robinhood",tier="keyed"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="quicknode",chain="robinhood",tier="keyed"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="quicknode",chain="robinhood",tier="keyed"}[1h])) - slug: alchemy name: Alchemy tag: 30M CU/mo free, keyed Robinhood Chain endpoint - formula: "p50 over 24h of round-trip latency (ms) for eth_getBlockByNumber probed every 60s from US-East on Alchemy's keyed Robinhood Chain endpoint." + formula: "p50 over 24h of round-trip latency (ms) for eth_getBlockByNumber probed every 60s per region on Alchemy's keyed Robinhood Chain endpoint." queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) / sum(ocb:rpc_call:rate_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}[1h])) + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="alchemy",chain="robinhood",tier="keyed"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="alchemy",chain="robinhood",tier="keyed"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="alchemy",chain="robinhood",tier="keyed"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="alchemy",chain="robinhood",tier="keyed"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="alchemy",chain="robinhood",tier="keyed"}) / sum(ocb:rpc_call:rate_24h{provider="alchemy",chain="robinhood",tier="keyed"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="alchemy",chain="robinhood",tier="keyed"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="alchemy",chain="robinhood",tier="keyed"}[1h])) From 5abd8d93a33518eedaead0e97e801e095af81b8d Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:40:30 +0200 Subject: [PATCH 30/66] fee-compare: fix signed-carry gates, Paradex signed funding, GMX schema drift (#2224) --- src/app/api/fee-compare/route.ts | 113 +++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 34 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index 129b0059e..3af347efa 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -213,7 +213,9 @@ type RawGmxTrade = { fundingFeeAmount: string | null; pnlUsd: string | null; orderType: number; - indexToken: { symbol: string } | null; + // The subsquid schema dropped the indexToken relation; coins resolve from + // marketAddress via GMX_MARKETS instead. + marketAddress: string | null; }; type GmxWalletData = { @@ -433,21 +435,37 @@ async function fetchDydxCarryRates(): Promise { async function fetchParadexCarryRates(): Promise { const cached = carryRateCache["paradex"]; if (cached && Date.now() - cached.ts < RATE_CACHE_TTL_MS) return cached; - const res = await fetch("https://api.prod.paradex.trade/v1/markets", { - signal: AbortSignal.timeout(8000), - next: { revalidate: 3600 }, - }); - const data = (await res.json()) as { - results: Array<{ symbol: string; interest_rate?: string; funding_period_hours?: number | string }>; + // markets/summary carries the SIGNED per-period funding_rate (positive = longs pay). + // The plain /markets interest_rate is unsigned — feeding it into the signed carry + // model would systematically credit shorts, so it must not be used here. + const [summaryRes, marketsRes] = await Promise.all([ + fetch("https://api.prod.paradex.trade/v1/markets/summary?market=ALL", { + signal: AbortSignal.timeout(8000), + next: { revalidate: 3600 }, + }), + fetch("https://api.prod.paradex.trade/v1/markets", { + signal: AbortSignal.timeout(8000), + next: { revalidate: 3600 }, + }), + ]); + const summary = (await summaryRes.json()) as { + results: Array<{ symbol: string; funding_rate?: string }>; + }; + const markets = (await marketsRes.json()) as { + results: Array<{ symbol: string; funding_period_hours?: number | string }>; }; + const periodBySymbol: Record = {}; + for (const mkt of markets.results ?? []) { + periodBySymbol[mkt.symbol] = parseFloat(String(mkt.funding_period_hours ?? "8")) || 8; + } const fundingPerSecPerCoin: Record = {}; - for (const mkt of data.results ?? []) { + for (const mkt of summary.results ?? []) { if (!mkt.symbol.endsWith("-PERP")) continue; // "BTC-USD-PERP" → "BTC" const coin = mkt.symbol.replace(/-USD-PERP$/, "").replace(/-PERP$/, ""); - const periodHours = parseFloat(String(mkt.funding_period_hours ?? "8")) || 8; - const rate = parseFloat(mkt.interest_rate ?? "0") / (periodHours * 3600); - if (rate > 0) fundingPerSecPerCoin[coin] = rate; + const periodHours = periodBySymbol[mkt.symbol] ?? 8; + const rate = parseFloat(mkt.funding_rate ?? "") / (periodHours * 3600); + if (Number.isFinite(rate) && rate !== 0) fundingPerSecPerCoin[coin] = rate; } const result: CarryRates = { fundingPerSecPerCoin, borrowPerSecPerCoin: {}, ts: Date.now() }; carryRateCache["paradex"] = result; @@ -465,7 +483,11 @@ async function fetchEdgeXRate(): Promise<{ rate: number; makerRate: number; note }; const contracts = data.data?.contractList ?? []; const rates = contracts.map((c) => parseFloat(String(c.defaultTakerFeeRate ?? "0"))).filter((r) => r > 0); - const makerRates = contracts.map((c) => parseFloat(String(c.defaultMakerFeeRate ?? "0"))).filter((r) => r > 0); + // Zero is a legitimate maker rate (fee promos); only drop absent/unparsable values. + const makerRates = contracts + .filter((c) => c.defaultMakerFeeRate !== undefined && c.defaultMakerFeeRate !== null) + .map((c) => parseFloat(String(c.defaultMakerFeeRate))) + .filter((r) => Number.isFinite(r) && r >= 0); const rate = rates.length > 0 ? rates.reduce((a, b) => a + b, 0) / rates.length : 0.00038; const makerRate = makerRates.length > 0 ? makerRates.reduce((a, b) => a + b, 0) / makerRates.length : rate; const entry = { rate, makerRate, note: `${(rate * 10000).toFixed(2)} bps taker (live from EdgeX)`, ts: Date.now() }; @@ -818,7 +840,7 @@ async function fetchGmxTrades(wallet: string, cutoffMs: number): Promise= 0 && fundingFeesUsdc < 0.01 && Object.keys(gainsData.fundingPerSecPerCoin).length > 0) { + if (Math.abs(fundingFeesUsdc) < 0.01 && Object.keys(gainsData.fundingPerSecPerCoin).length > 0) { const gainsPositions = reconstructGainsPositions(usdcTrades, cutoffMs); const est = estimateGainsFundingFees(gainsPositions, gainsData.fundingPerSecPerCoin); - if (est > 0.01) { + if (Math.abs(est) > 0.01) { fundingFeesUsdc = est; fundingEstimated = true; } @@ -1788,6 +1812,9 @@ export async function GET(req: Request) { const venueBResult = buildVenueResult(venueB, rateB, noteB, rateIsLiveB); const comparison: ComparisonResult = { aToBSim: null, bToASim: null }; + // An effective rate derived from the wallet's OWN fills on a venue must not be + // overwritten by a projection computed from the other venue's history. + let aActualRateSet = false; // aToBSim: venueA actual fills vs simulated venueB cost (with carry projection) if (venueAResult.wallet !== null) { @@ -1797,6 +1824,7 @@ export async function GET(req: Request) { comparison.aToBSim = r.sim; venueAResult.effectiveRateBps = r.hlNetBps; venueAResult.effectiveRateNote = `${r.hlNetBps.toFixed(2)} bps net (fees + funding)`; + aActualRateSet = true; venueBResult.effectiveRateBps = r.gainsEffBps; venueBResult.effectiveRateNote = `${r.gainsEffBps.toFixed(2)} bps effective (your coins)`; } @@ -1844,12 +1872,13 @@ export async function GET(req: Request) { }; } - // HL→GMX: use wallet's own GMX history as carry proxy + // HL→GMX: use wallet's own GMX history as carry proxy. + // Funding stays SIGNED: net funding received on GMX projects as a credit. const gmxForA = gmxWalletData as GmxWalletData | null; if (venueA === "hyperliquid" && venueB === "gmx-v2" && gmxForA !== null && gmxForA.notionalUsd > 0) { const takerFees = equivFees; const gmxBorrowRate = gmxForA.borrowingFeesUsdc / gmxForA.notionalUsd; - const gmxFundingRate = Math.max(0, gmxForA.fundingFeesUsdc) / gmxForA.notionalUsd; + const gmxFundingRate = gmxForA.fundingFeesUsdc / gmxForA.notionalUsd; const gmxBorrowProj = stats.notional * gmxBorrowRate; const gmxFundingProj = stats.notional * gmxFundingRate; equivFees += gmxBorrowProj + gmxFundingProj; @@ -1858,15 +1887,17 @@ export async function GET(req: Request) { borrowFees: gmxBorrowProj, fundingFees: gmxFundingProj, borrowProjected: gmxBorrowProj > 0.01, - fundingProjected: gmxFundingProj > 0.01, + fundingProjected: Math.abs(gmxFundingProj) > 0.01, }; } - // Generic carry: dYdX, Paradex, and any future venue with rate data + // Generic carry: dYdX, Paradex, and any future venue with rate data. + // Math.abs on funding: a pure credit (negative fundingFees) must still be + // projected — dropping it would bias the comparison toward the source venue. if (!projectedCarry && positions.length > 0) { const bCarry = getVenueCarryRates(venueB); const { borrowFees, fundingFees } = estimateCarryFees(positions, bCarry); - if (borrowFees > 0.001 || fundingFees > 0.001) { + if (borrowFees > 0.001 || Math.abs(fundingFees) > 0.001) { const takerFees = equivFees; equivFees += borrowFees + fundingFees; projectedCarry = { @@ -1874,7 +1905,7 @@ export async function GET(req: Request) { borrowFees, fundingFees, borrowProjected: borrowFees > 0.01, - fundingProjected: fundingFees > 0.01, + fundingProjected: Math.abs(fundingFees) > 0.01, }; } } @@ -1888,9 +1919,12 @@ export async function GET(req: Request) { projectedCarry, }; if (stats.notional > 0) { + const bBps = (equivFees / stats.notional) * 10000; venueAResult.effectiveRateBps = (stats.fees / stats.notional) * 10000; venueAResult.effectiveRateNote = `${((stats.fees / stats.notional) * 10000).toFixed(2)} bps actual (your fills)`; - venueBResult.effectiveRateBps = (equivFees / stats.notional) * 10000; + aActualRateSet = true; + venueBResult.effectiveRateBps = bBps; + venueBResult.effectiveRateNote = `${bBps.toFixed(2)} bps projected (your ${venueAResult.name} trades)`; } } } @@ -1904,8 +1938,11 @@ export async function GET(req: Request) { comparison.bToASim = r.sim; venueBResult.effectiveRateBps = r.hlNetBps; venueBResult.effectiveRateNote = `${r.hlNetBps.toFixed(2)} bps net (fees + funding)`; - venueAResult.effectiveRateBps = r.gainsEffBps; - venueAResult.effectiveRateNote = `${r.gainsEffBps.toFixed(2)} bps effective (your coins)`; + // Don't overwrite the Gains wallet's own-fills rate with the HL-derived projection. + if (!aActualRateSet) { + venueAResult.effectiveRateBps = r.gainsEffBps; + venueAResult.effectiveRateNote = `${r.gainsEffBps.toFixed(2)} bps effective (your coins)`; + } } } else { const stats = walletStats(venueB, venueBResult.wallet, venueA); @@ -1952,11 +1989,12 @@ export async function GET(req: Request) { } // HL→GMX (venueB=HL, venueA=GMX) + // Funding stays SIGNED: net funding received on GMX projects as a credit. const gmxForB = gmxWalletData as GmxWalletData | null; if (venueB === "hyperliquid" && venueA === "gmx-v2" && gmxForB !== null && gmxForB.notionalUsd > 0) { const takerFees = equivFees; const gmxBorrowRate = gmxForB.borrowingFeesUsdc / gmxForB.notionalUsd; - const gmxFundingRate = Math.max(0, gmxForB.fundingFeesUsdc) / gmxForB.notionalUsd; + const gmxFundingRate = gmxForB.fundingFeesUsdc / gmxForB.notionalUsd; const gmxBorrowProj = stats.notional * gmxBorrowRate; const gmxFundingProj = stats.notional * gmxFundingRate; equivFees += gmxBorrowProj + gmxFundingProj; @@ -1965,15 +2003,17 @@ export async function GET(req: Request) { borrowFees: gmxBorrowProj, fundingFees: gmxFundingProj, borrowProjected: gmxBorrowProj > 0.01, - fundingProjected: gmxFundingProj > 0.01, + fundingProjected: Math.abs(gmxFundingProj) > 0.01, }; } - // Generic carry projection + // Generic carry projection. + // Math.abs on funding: a pure credit (negative fundingFees) must still be + // projected — dropping it would bias the comparison toward the source venue. if (!projectedCarry && positions.length > 0) { const aCarry = getVenueCarryRates(venueA); const { borrowFees, fundingFees } = estimateCarryFees(positions, aCarry); - if (borrowFees > 0.001 || fundingFees > 0.001) { + if (borrowFees > 0.001 || Math.abs(fundingFees) > 0.001) { const takerFees = equivFees; equivFees += borrowFees + fundingFees; projectedCarry = { @@ -1981,7 +2021,7 @@ export async function GET(req: Request) { borrowFees, fundingFees, borrowProjected: borrowFees > 0.01, - fundingProjected: fundingFees > 0.01, + fundingProjected: Math.abs(fundingFees) > 0.01, }; } } @@ -1997,7 +2037,12 @@ export async function GET(req: Request) { if (stats.notional > 0) { venueBResult.effectiveRateBps = (stats.fees / stats.notional) * 10000; venueBResult.effectiveRateNote = `${((stats.fees / stats.notional) * 10000).toFixed(2)} bps actual (your fills)`; - venueAResult.effectiveRateBps = (equivFees / stats.notional) * 10000; + // Don't overwrite venueA's own-fills rate with a projection from venueB's history. + if (!aActualRateSet) { + const aBps = (equivFees / stats.notional) * 10000; + venueAResult.effectiveRateBps = aBps; + venueAResult.effectiveRateNote = `${aBps.toFixed(2)} bps projected (your ${venueBResult.name} trades)`; + } } } } From 43bd73c75d2f26def3f685e7d65fe4c618a2b5f7 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:07:42 +0200 Subject: [PATCH 31/66] fix(bench-243): per-provider regions queries so the region picker renders --- benchmarks/keyed-rpc-robinhood.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/benchmarks/keyed-rpc-robinhood.yml b/benchmarks/keyed-rpc-robinhood.yml index b92414e9e..f4c3edabf 100644 --- a/benchmarks/keyed-rpc-robinhood.yml +++ b/benchmarks/keyed-rpc-robinhood.yml @@ -81,6 +81,13 @@ providers: success: sum(ocb:rpc_call:ok_rate_24h{provider="chainstack",chain="robinhood",tier="keyed"}) / sum(ocb:rpc_call:rate_24h{provider="chainstack",chain="robinhood",tier="keyed"}) sample_size: sum(ocb:rpc_call:increase_24h{provider="chainstack",chain="robinhood",tier="keyed"}) series: avg(avg_over_time(rpc_latency_milliseconds{provider="chainstack",chain="robinhood",tier="keyed"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}[1h]) + - region: sgp + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="chainstack",chain="robinhood",tier="keyed",region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="chainstack",chain="robinhood",tier="keyed",region="sgp"}[1h]) - slug: quicknode name: QuickNode @@ -94,6 +101,13 @@ providers: success: sum(ocb:rpc_call:ok_rate_24h{provider="quicknode",chain="robinhood",tier="keyed"}) / sum(ocb:rpc_call:rate_24h{provider="quicknode",chain="robinhood",tier="keyed"}) sample_size: sum(ocb:rpc_call:increase_24h{provider="quicknode",chain="robinhood",tier="keyed"}) series: avg(avg_over_time(rpc_latency_milliseconds{provider="quicknode",chain="robinhood",tier="keyed"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}[1h]) + - region: sgp + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="quicknode",chain="robinhood",tier="keyed",region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="quicknode",chain="robinhood",tier="keyed",region="sgp"}[1h]) - slug: alchemy name: Alchemy @@ -107,3 +121,10 @@ providers: success: sum(ocb:rpc_call:ok_rate_24h{provider="alchemy",chain="robinhood",tier="keyed"}) / sum(ocb:rpc_call:rate_24h{provider="alchemy",chain="robinhood",tier="keyed"}) sample_size: sum(ocb:rpc_call:increase_24h{provider="alchemy",chain="robinhood",tier="keyed"}) series: avg(avg_over_time(rpc_latency_milliseconds{provider="alchemy",chain="robinhood",tier="keyed"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}[1h]) + - region: sgp + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="alchemy",chain="robinhood",tier="keyed",region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="alchemy",chain="robinhood",tier="keyed",region="sgp"}[1h]) From a9447d5d987bf5e5bfcf66f7cd086bb86dc6e4af Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:17:36 +0200 Subject: [PATCH 32/66] =?UTF-8?q?feat(spec):=20aggregate=5Ffilters=20?= =?UTF-8?q?=E2=80=94=20pin=20the=20unfiltered=20build=20to=20a=20dimension?= =?UTF-8?q?=20slice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- benchmarks/keyed-rpc-robinhood.yml | 10 ++++++++-- src/lib/materialize/load.ts | 18 +++++++++++++++--- src/lib/spec-schema.ts | 21 +++++++++++++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/benchmarks/keyed-rpc-robinhood.yml b/benchmarks/keyed-rpc-robinhood.yml index f4c3edabf..9193ac4ee 100644 --- a/benchmarks/keyed-rpc-robinhood.yml +++ b/benchmarks/keyed-rpc-robinhood.yml @@ -46,11 +46,17 @@ findings: # fixed for the whole bench, so cells key on provider and region. rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="robinhood",tier="keyed"}) +# Headline view pinned to the Singapore probe: the default (unfiltered) +# build, TL;DR, JSON-LD and OG cite the sgp slice; the US-East tab +# re-scopes on click. No "all" option on purpose — an average across a +# healthy region and a degraded one describes neither. +aggregate_filters: + region: sgp + dimensions: region: - - { value: all, label: All regions } - - { value: us-east, label: US-East } - { value: sgp, label: Singapore } + - { value: us-east, label: US-East } faq: - q: "Which Robinhood Chain RPC provider is fastest?" diff --git a/src/lib/materialize/load.ts b/src/lib/materialize/load.ts index 5b14517c5..b9ea67505 100644 --- a/src/lib/materialize/load.ts +++ b/src/lib/materialize/load.ts @@ -164,9 +164,21 @@ export async function specToBenchmark( ): Promise { const editorial = buildEditorial(spec); - const activeLabels = activeFilterLabels(options); - const isFiltered = Object.keys(activeLabels).length > 0; - const filteredSpec = isFiltered ? applyDimensionsToSpec(spec, activeLabels) : spec; + // Merge spec-declared aggregate defaults UNDER the reader's filters: + // an explicit ?region= / tab selection always wins over the pin. The + // unfiltered-view semantics below (provider augmentation, "All" copy) + // key on the reader's filters only, so a pinned aggregate still reads + // as the bench's headline view rather than a filtered slice. + const merged: BenchmarkFilters = { + ...((spec.aggregate_filters ?? {}) as BenchmarkFilters), + ...options, + }; + const activeLabels = activeFilterLabels(merged); + const isFiltered = Object.keys(activeFilterLabels(options)).length > 0; + const filteredSpec = + Object.keys(activeLabels).length > 0 + ? applyDimensionsToSpec(spec, activeLabels) + : spec; const live = await tryLoadLive(filteredSpec, isFiltered); if (live) { diff --git a/src/lib/spec-schema.ts b/src/lib/spec-schema.ts index 63b52a504..92306c02b 100644 --- a/src/lib/spec-schema.ts +++ b/src/lib/spec-schema.ts @@ -382,6 +382,27 @@ export const SpecSchema = z }) .optional(), + /** + * Default dimension scope for the unfiltered ("aggregate") build. + * When set, the tier-A snapshot (sig "") is built with these labels + * injected into every query selector, so the bench's headline view, + * TL;DR, JSON-LD and OG all cite the scoped slice instead of an + * average across dimensions. A reader-applied filter (region tab, + * ?region= variant) always wins over these defaults. Keys must be + * dimension keys (region/chain/kind/venue); values follow the same + * PromQL label-value alphabet as dimension values. + * First use: keyed-rpc-robinhood pins region=sgp so the default view + * is the Singapore probe while the US-East tab stays available. */ + aggregate_filters: z + .object({ + chain: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/).optional(), + region: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/).optional(), + kind: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/).optional(), + venue: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/).optional(), + }) + .strict() + .optional(), + /** * Optional per-provider chip labels rendered next to the provider * name in the ranking row. Keys are provider slugs, values are the From 79b9963c750834ae707281eb79e1a44edcebdc8b Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:35:33 +0200 Subject: [PATCH 33/66] =?UTF-8?q?fix(bench-243):=20prod=20is=20Singapore-o?= =?UTF-8?q?nly=20=E2=80=94=20no=20dims=20inherited=20from=20snapshot,=20ab?= =?UTF-8?q?stract=20names=20the=20probe=20region?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 9555bb76079c70f8d673d82c33dab1112fcab4fa) --- src/lib/spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/spec.ts b/src/lib/spec.ts index d873e51a2..fd55092d4 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -131,8 +131,12 @@ export function overlayEditorial(stored: Benchmark, spec: Spec): Benchmark { // too so newly added dimension values (e.g. region opts added to // an existing bench) surface immediately on the compare matrix // and the bench page filters without waiting on the materialise - // worker to rewrite the snapshot. - dimensions: spec.dimensions ?? stored.dimensions, + // worker to rewrite the snapshot. No `?? stored` fallback: the + // live YAML is the source of truth, so a spec that declares NO + // dimensions must not inherit tabs from a snapshot written by a + // worker running a divergent branch (keyed-rpc-robinhood: main is + // Singapore-only while dev/worker carries the region dims). + dimensions: spec.dimensions, // provider_notes is a YAML editorial declaration: drives the // per-provider chip rendered next to the name in the ranking row. // Overlay so newly added notes surface immediately without waiting From 1c84855215061a57cca227598676bfaebb927fc9 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:52:34 +0200 Subject: [PATCH 34/66] fix: by-region grid driven by live spec (port from main) --- src/components/region-grid.tsx | 29 +++++++++++++++++++++++++++-- src/lib/materialize/load.ts | 1 + src/lib/spec.ts | 3 +++ src/types/benchmark.ts | 9 +++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/components/region-grid.tsx b/src/components/region-grid.tsx index eeeb86091..78838d8bb 100644 --- a/src/components/region-grid.tsx +++ b/src/components/region-grid.tsx @@ -7,15 +7,40 @@ import { buildProviderColors } from "@/lib/series-colors"; type Props = { benchmark: Benchmark }; -const REGIONS = [ +const LEGACY_REGIONS = [ { key: "us-east", label: "US-East" }, { key: "eu-west", label: "EU-West" }, { key: "ap-southeast", label: "AP-Southeast" }, ] as const; +const REGION_LABELS: Record = { + "us-east": "US-East", + "us-west": "US-West", + "eu-west": "EU-West", + "ap-southeast": "AP-Southeast", + sgp: "Singapore", + global: "Global", +}; + +/** Columns are driven by the live spec, not by whatever regions the + * snapshot happens to carry (prod and staging share worker snapshots, + * so a divergent branch's regions would otherwise leak): + * 1. dimensions.region declared → those columns, in declared order. + * 2. aggregate_filters.region pinned → that single column. + * 3. neither → the legacy fixed three-column layout. */ +function regionColumns(b: Benchmark): { key: string; label: string }[] { + const dims = (b.dimensions?.region ?? []).filter((r) => r.value !== "all"); + if (dims.length > 0) return dims.map((r) => ({ key: r.value, label: r.label })); + const pinned = b.aggregateFilters?.region; + if (pinned) return [{ key: pinned, label: REGION_LABELS[pinned] ?? pinned }]; + return [...LEGACY_REGIONS]; +} + export function RegionGrid({ benchmark }: Props) { const { results, unit, extras } = benchmark; + const REGIONS = useMemo(() => regionColumns(benchmark), [benchmark]); + // Both maps recompute O(n*m) over results × regions. Memoise so the // grid doesn't reprice every cell on each parent re-render (parent // re-renders on every chain/region tab change and every chart view @@ -32,7 +57,7 @@ export function RegionGrid({ benchmark }: Props) { map.set(region.key, m); } return map; - }, [results, extras.regions]); + }, [results, extras.regions, REGIONS]); if (!results.length) return null; diff --git a/src/lib/materialize/load.ts b/src/lib/materialize/load.ts index b9ea67505..760c93ce3 100644 --- a/src/lib/materialize/load.ts +++ b/src/lib/materialize/load.ts @@ -145,6 +145,7 @@ export function buildEditorial( findings: spec.findings, source: spec.source, dimensions: spec.dimensions, + aggregateFilters: spec.aggregate_filters, ledgerColumns: spec.ledger_columns, providerNotes: spec.provider_notes, }; diff --git a/src/lib/spec.ts b/src/lib/spec.ts index fd55092d4..d1548fbe3 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -137,6 +137,9 @@ export function overlayEditorial(stored: Benchmark, spec: Spec): Benchmark { // worker running a divergent branch (keyed-rpc-robinhood: main is // Singapore-only while dev/worker carries the region dims). dimensions: spec.dimensions, + // Same source-of-truth rule as dimensions: the live YAML decides + // the aggregate pin, never the snapshot. + aggregateFilters: spec.aggregate_filters, // provider_notes is a YAML editorial declaration: drives the // per-provider chip rendered next to the name in the ranking row. // Overlay so newly added notes surface immediately without waiting diff --git a/src/types/benchmark.ts b/src/types/benchmark.ts index 908ae00f4..11cc3c6df 100644 --- a/src/types/benchmark.ts +++ b/src/types/benchmark.ts @@ -244,6 +244,15 @@ export type Benchmark = { kind?: { value: string; label: string }[]; venue?: { value: string; label: string }[]; }; + /** Default dimension scope for the unfiltered build (spec + * `aggregate_filters`). Presentation surfaces (e.g. the by-region + * grid) also read it to restrict what a single-vantage bench shows. */ + aggregateFilters?: { + chain?: string; + region?: string; + kind?: string; + venue?: string; + }; category: "Aggregators" | "Bridges" | "Blockchains" | "Trading" | "Wallets" | "RPCs" | "NFT APIs" | "Explorers" | "RWA"; results: ProviderResult[]; /** Per-chain leader, computed only on the unfiltered ("All chains") view From 54a221f6d76f815ed6fddb8c9f62906622bd3230 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:52:55 +0200 Subject: [PATCH 35/66] report: align Robinhood RPC report on Singapore probe and current medians --- .../2026-08-fastest-robinhood-chain-rpc.mdx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/content/reports/rpc/2026-08-fastest-robinhood-chain-rpc.mdx b/src/content/reports/rpc/2026-08-fastest-robinhood-chain-rpc.mdx index ac0b0f9f3..8a8430e87 100644 --- a/src/content/reports/rpc/2026-08-fastest-robinhood-chain-rpc.mdx +++ b/src/content/reports/rpc/2026-08-fastest-robinhood-chain-rpc.mdx @@ -5,7 +5,7 @@ slug: "2026-08-fastest-robinhood-chain-rpc" publishedAt: "2026-08-31" period: "August 2026" summary: "Robinhood Chain is live, and three providers offer keyed endpoints for it. We benchmarked all three on the same call, the same cadence, over a rolling 24-hour window. Here is which one delivers the fastest median read." -heroFinding: "Chainstack posts the lowest median latency on Robinhood Chain, roughly 7 ms p50 over 24 hours, the best of any keyed provider on the chain." +heroFinding: "Chainstack posts the lowest median latency on Robinhood Chain, roughly 3 ms p50 over 24 hours, the best of any keyed provider on the chain." author: "OpenChainBench Research" readingTime: 6 canonical: "https://openchainbench.com/reports/rpc/2026-08-fastest-robinhood-chain-rpc" @@ -14,8 +14,8 @@ canonical: "https://openchainbench.com/reports/rpc/2026-08-fastest-robinhood-cha - Robinhood Chain (chain ID 4663) is an Arbitrum Orbit L2 for tokenized equity trading, live on mainnet since July 2026 with a gas subsidy running through September 29, 2026. - Three providers currently expose keyed Robinhood Chain endpoints: Chainstack, Alchemy, and QuickNode. This bench probes all three. -- Chainstack posts the lowest median latency in the cohort, roughly 7 ms (p50, 24h). -- Alchemy follows at ~17 ms, QuickNode at ~71 ms. All three returned a usable block on 100% of probes. +- Chainstack posts the lowest median latency in the cohort, roughly 3 ms (p50, 24h). +- Alchemy follows at ~5 ms, QuickNode at ~68 ms. All three returned a usable block on 100% of probes. - This is the first independent public latency benchmark for Robinhood Chain. @@ -23,7 +23,7 @@ canonical: "https://openchainbench.com/reports/rpc/2026-08-fastest-robinhood-cha Every number here is a live query against the [OpenChainBench Prometheus](https://openchainbench.com/methodology). The harness sends each provider an `eth_getBlockByNumber("latest", false)` request every 60 seconds, with a rotating JSON-RPC id so no edge cache can serve a canned response. Each probe forces a real Robinhood Chain tip. -Each response is classified as ok, http_err, jsonrpc_err, stale (block more than 20 behind the cross-provider tip), or timeout. Latency is recorded only for ok responses, and the figures below are the p50 over a rolling 24-hour window. Probes run from US-East (Virginia), where our infrastructure sits; that is the reference region for this bench. +Each response is classified as ok, http_err, jsonrpc_err, stale (block more than 20 behind the cross-provider tip), or timeout. Latency is recorded only for ok responses, and the figures below are the p50 over a rolling 24-hour window. Probes run from Singapore, the closest probe region to the chain's Asia-Pacific infrastructure; that is the reference region for this bench. Live page, sparklines, and source: [keyed-rpc-robinhood](/benchmarks/keyed-rpc-robinhood). @@ -31,26 +31,26 @@ Live page, sparklines, and source: [keyed-rpc-robinhood](/benchmarks/keyed-rpc-r Three providers offer keyed Robinhood Chain endpoints today, and all three answer reliably. None dropped a probe over the window. So this is a clean speed comparison, and on the median the gap is wide. -Chainstack returns a Robinhood Chain block in roughly 7 ms at the median over 24 hours, the lowest p50 of any keyed provider on the chain. At the median it clears requests more than twice as fast as the next provider in the cohort. +Chainstack returns a Robinhood Chain block in roughly 3 ms at the median over 24 hours, the lowest p50 of any keyed provider on the chain. Its Global Node sits closest to the chain's infrastructure and it shows in the round-trip. - + -Alchemy lands second at ~17 ms via its edge infrastructure. QuickNode's shared-endpoint fleet sits at ~71 ms. All three cleared 100% of probes over the window, so on availability the field is even; the separation is in how fast the typical read comes back. +Alchemy lands second at ~5 ms via its edge infrastructure. QuickNode's shared-endpoint fleet sits at ~68 ms. All three cleared 100% of probes over the window, so on availability the field is even; the separation is in how fast the typical read comes back. ## Why Median Latency Matters Here Robinhood Chain is a financially sensitive chain: it exists to move tokenized equities, and blocks arrive roughly every 100 ms. A read that lags the tip is a read against a stale price. For the calls an application makes most, the median round-trip is what most of those reads actually experience, so it is a direct input to how fresh your view of the chain is, whether that is a quote engine, a trading frontend, or any component that polls the head. -On the 24-hour median, Chainstack leaves the most headroom against that 100 ms cadence of the three benched providers. +At ~3 ms median, Chainstack leaves the most headroom against that 100 ms cadence of the three benched providers, and both leaders sit comfortably inside a single block interval. ## Which to Use -Chainstack is the median leader in this cohort, roughly 7 ms p50 over 24h from US-East. If your priority is the fastest typical read on Robinhood Chain, it is the front-runner today. As with any single endpoint, pair it with a fallback so a provider-side incident never leaves you without a tip. +Chainstack is the median leader in this cohort, roughly 3 ms p50 over 24h. If your priority is the fastest typical read on Robinhood Chain, it is the front-runner today. As with any single endpoint, pair it with a fallback so a provider-side incident never leaves you without a tip. -Both answer reliably on Robinhood Chain (100% success over the window). Alchemy at ~17 ms and QuickNode at ~71 ms are workable; the trade-off against Chainstack is median latency, not availability. +Both answer reliably on Robinhood Chain (100% success over the window). Alchemy at ~5 ms is close behind the leader; QuickNode at ~68 ms is workable for less latency-sensitive reads. The trade-off against Chainstack is median latency, not availability. ## What to Watch Next @@ -64,4 +64,4 @@ Robinhood Chain is a new chain, and the keyed-provider field is still small. As - **License:** CC BY 4.0, reproduce with attribution to OpenChainBench and a link to the canonical URL. - **Corrections:** file a [GitHub issue](https://github.com/ChainBench/OpenChainBench/issues/new). -*Figures are the 24h median as of 2026-08-31 and move as new samples land. Check the live page for current numbers.* +*Figures are the 24h median as of 2026-09-02 and move as new samples land. Check the live page for current numbers.* From d31a56960563741bec36a53bf9c00365c61b17b5 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:57:33 +0200 Subject: [PATCH 36/66] =?UTF-8?q?fix(bench-243):=20dev=20texts=20=E2=80=94?= =?UTF-8?q?=20discreet=20region=20wording,=20headline=20cites=20Singapore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- benchmarks/keyed-rpc-robinhood.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/benchmarks/keyed-rpc-robinhood.yml b/benchmarks/keyed-rpc-robinhood.yml index 9193ac4ee..a12960dae 100644 --- a/benchmarks/keyed-rpc-robinhood.yml +++ b/benchmarks/keyed-rpc-robinhood.yml @@ -4,8 +4,8 @@ slug: keyed-rpc-robinhood number: "243" title: "Fastest Robinhood Chain RPC with API key: Chainstack vs Alchemy vs QuickNode" seo_title: "Fastest Robinhood Chain RPC API key 2026: Chainstack vs Alchemy vs QuickNode latency" -seo_description: "Live Robinhood Chain RPC latency: Chainstack, Alchemy, and QuickNode keyed endpoints probed every 60s from US-East. First public benchmark for this Arbitrum Orbit L2." -subtitle: eth_getBlockByNumber latency against keyed Robinhood Chain endpoints (Chainstack, Alchemy, QuickNode), probed every 60 seconds from US-East. +seo_description: "Live Robinhood Chain RPC latency: Chainstack, Alchemy, and QuickNode keyed endpoints probed every 60s. First public benchmark for this Arbitrum Orbit L2." +subtitle: eth_getBlockByNumber latency against keyed Robinhood Chain endpoints (Chainstack, Alchemy, QuickNode), probed every 60 seconds. category: RPCs status: live metric: RPC latency @@ -19,7 +19,7 @@ seo_intro: | chain with low tolerance for RPC latency. This bench probes Chainstack, Alchemy, and QuickNode on their keyed Robinhood Chain endpoints every 60 seconds from two probe regions - (US-East and Singapore). Chainstack posts the lowest median latency. This is the only independently-run public + (Singapore and US-East). Chainstack posts the lowest median latency. This is the only independently-run public latency benchmark for Robinhood Chain. abstract: | @@ -31,7 +31,7 @@ abstract: | the latency distribution. methodology: - - "Cadence: every 60 seconds per provider from each probe region (US-East Virginia; Singapore). Headline numbers average both regions; the region tabs re-scope every figure to one origin. Robinhood Chain runs on the Arbitrum Orbit stack with ~100ms block times; the 60s probe cadence captures 600 blocks per hour." + - "Cadence: every 60 seconds per provider from each probe region (Singapore; US-East Virginia). Headline numbers cite the Singapore probe, the closest region to the chain's Asia-Pacific infrastructure; the region tabs re-scope every figure to one origin. Robinhood Chain runs on the Arbitrum Orbit stack with ~100ms block times; the 60s probe cadence captures 600 blocks per hour." - "Payload: eth_getBlockByNumber('latest', false) with a rotating JSON-RPC id. Non-cacheable by design: the rotating id defeats body-keyed edge caches and forces the provider to serve a real Robinhood Chain tip." - "Authentication: Chainstack Global Node key (node-scoped, dedicated to Robinhood Chain; key in Railway env, not in repo). Alchemy uses its standard multi-chain key. QuickNode uses its shared-endpoint key for Robinhood Chain. All keys in Railway env, not in repo." - "Call-result classification: ok (HTTP 200 with usable block number), http_err, jsonrpc_err (HTTP 200 with error body), stale (block more than 20 behind the cross-provider tip), timeout. Latency is recorded only for ok responses." @@ -66,7 +66,7 @@ faq: - q: "Why is this the only Robinhood Chain latency benchmark?" a: "Robinhood Chain is a new chain. OpenChainBench added it within days of Chainstack, Alchemy, and QuickNode support going live, making this the first and so far only independent latency measurement. Infura and Ankr do not currently list Robinhood Chain in their supported networks." - q: "Which regions does this benchmark probe from?" - a: "Two origins: US-East (Virginia) and Singapore. The headline figures average both; the region tabs at the top of the page re-scope every number to a single origin. Pick the tab closest to where your requests originate." + a: "Two origins: Singapore and US-East (Virginia). The headline figures cite the Singapore probe, the closest region to the chain's Asia-Pacific infrastructure; the region tabs at the top of the page re-scope every number to a single origin." source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-keyed-latency From 4a82f937a471704ff31ceb112495259db1cc3fe3 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:47:41 +0200 Subject: [PATCH 37/66] feat: add OG image cards for all hub pages missing dedicated cards (#2234) --- src/app/alternatives/opengraph-image.tsx | 15 +++++++++++++++ src/app/answers/opengraph-image.tsx | 15 +++++++++++++++ src/app/badges/opengraph-image.tsx | 15 +++++++++++++++ src/app/bridge/opengraph-image.tsx | 15 +++++++++++++++ src/app/chains/opengraph-image.tsx | 15 +++++++++++++++ src/app/compare/opengraph-image.tsx | 15 +++++++++++++++ src/app/data-api/opengraph-image.tsx | 15 +++++++++++++++ src/app/fee-compare/opengraph-image.tsx | 15 +++++++++++++++ src/app/hyperliquid/opengraph-image.tsx | 15 +++++++++++++++ src/app/partners/opengraph-image.tsx | 15 +++++++++++++++ src/app/perps/opengraph-image.tsx | 15 +++++++++++++++ src/app/prediction-markets/opengraph-image.tsx | 15 +++++++++++++++ src/app/reports/opengraph-image.tsx | 15 +++++++++++++++ src/app/rpc/opengraph-image.tsx | 15 +++++++++++++++ src/app/team/opengraph-image.tsx | 15 +++++++++++++++ src/app/trading-apps/opengraph-image.tsx | 15 +++++++++++++++ 16 files changed, 240 insertions(+) create mode 100644 src/app/alternatives/opengraph-image.tsx create mode 100644 src/app/answers/opengraph-image.tsx create mode 100644 src/app/badges/opengraph-image.tsx create mode 100644 src/app/bridge/opengraph-image.tsx create mode 100644 src/app/chains/opengraph-image.tsx create mode 100644 src/app/compare/opengraph-image.tsx create mode 100644 src/app/data-api/opengraph-image.tsx create mode 100644 src/app/fee-compare/opengraph-image.tsx create mode 100644 src/app/hyperliquid/opengraph-image.tsx create mode 100644 src/app/partners/opengraph-image.tsx create mode 100644 src/app/perps/opengraph-image.tsx create mode 100644 src/app/prediction-markets/opengraph-image.tsx create mode 100644 src/app/reports/opengraph-image.tsx create mode 100644 src/app/rpc/opengraph-image.tsx create mode 100644 src/app/team/opengraph-image.tsx create mode 100644 src/app/trading-apps/opengraph-image.tsx diff --git a/src/app/alternatives/opengraph-image.tsx b/src/app/alternatives/opengraph-image.tsx new file mode 100644 index 000000000..e7e60e023 --- /dev/null +++ b/src/app/alternatives/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "Alternatives to crypto infrastructure products, ranked by live OpenChainBench benchmarks."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Alternatives", + headline: "Alternatives, by the numbers.", + subline: + "Benchmark-ranked alternatives to every major crypto infrastructure product. Same data, reframed per product, no verdict.", + }); +} diff --git a/src/app/answers/opengraph-image.tsx b/src/app/answers/opengraph-image.tsx new file mode 100644 index 000000000..f9084db07 --- /dev/null +++ b/src/app/answers/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "OpenChainBench answers. Common questions about crypto infrastructure, answered with live benchmark data."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Answers", + headline: "Questions, answered with data.", + subline: + "Common questions about crypto infrastructure performance, answered directly from live OpenChainBench benchmark results.", + }); +} diff --git a/src/app/badges/opengraph-image.tsx b/src/app/badges/opengraph-image.tsx new file mode 100644 index 000000000..eaa9f78c5 --- /dev/null +++ b/src/app/badges/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "OpenChainBench live ranking badges. Embed a live benchmark rank badge in your docs or README."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Badges", + headline: "Show your live rank.", + subline: + "Embed a live OpenChainBench ranking badge in your docs, README or website. Updates automatically as benchmark data changes.", + }); +} diff --git a/src/app/bridge/opengraph-image.tsx b/src/app/bridge/opengraph-image.tsx new file mode 100644 index 000000000..48d466bef --- /dev/null +++ b/src/app/bridge/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "Cheapest cross-chain bridge 2026. Live fee and slippage ranking across Across, deBridge, LI.FI, Relay and more."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Bridge benchmarks", + headline: "Cheapest cross-chain bridge, live.", + subline: + "All-in fee (fees + slippage + destination gas) for $300 USDC across Solana, Base and Arbitrum corridors. Refreshed every 5 minutes.", + }); +} diff --git a/src/app/chains/opengraph-image.tsx b/src/app/chains/opengraph-image.tsx new file mode 100644 index 000000000..7a4251e0e --- /dev/null +++ b/src/app/chains/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "Chains tracked by OpenChainBench. Browse all live benchmarks grouped by blockchain."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Chains", + headline: "Every chain we measure.", + subline: + "All blockchains tracked by OpenChainBench. Pick a chain for the full set of live RPC, finality and data measurements.", + }); +} diff --git a/src/app/compare/opengraph-image.tsx b/src/app/compare/opengraph-image.tsx new file mode 100644 index 000000000..bc6a36158 --- /dev/null +++ b/src/app/compare/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "Compare crypto infrastructure providers head to head. Live benchmark data, no vendor claims."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Compare", + headline: "Providers, head to head.", + subline: + "Pick any two infrastructure providers and compare them on latency, reliability and cost. Live data, no vendor claims.", + }); +} diff --git a/src/app/data-api/opengraph-image.tsx b/src/app/data-api/opengraph-image.tsx new file mode 100644 index 000000000..502014c0c --- /dev/null +++ b/src/app/data-api/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "Best crypto data API 2026. Live benchmark ranking of price, NFT and DeFi data providers by latency and accuracy."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Data API benchmarks", + headline: "Best crypto data API, ranked.", + subline: + "Latency, accuracy and reliability for every major crypto data API provider. Measured continuously from three regions.", + }); +} diff --git a/src/app/fee-compare/opengraph-image.tsx b/src/app/fee-compare/opengraph-image.tsx new file mode 100644 index 000000000..f5e261b88 --- /dev/null +++ b/src/app/fee-compare/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "Perp DEX fee comparison. Compare taker fees between any two venues using real on-chain wallet data."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Fee compare", + headline: "Perp DEX fees, head to head.", + subline: + "Paste a wallet and compare what you paid on Hyperliquid or Gains against any other venue. Live on-chain data, no API key.", + }); +} diff --git a/src/app/hyperliquid/opengraph-image.tsx b/src/app/hyperliquid/opengraph-image.tsx new file mode 100644 index 000000000..c3219d39f --- /dev/null +++ b/src/app/hyperliquid/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "Hyperliquid frontends and HIP-3 DEX leaderboard. Live revenue, volume and users from a local HL node."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Hyperliquid", + headline: "Every HL frontend, ranked.", + subline: + "Revenue, volume and daily users for every Hyperliquid frontend and HIP-3 deployer. Server-side data from a local node tailing every fill.", + }); +} diff --git a/src/app/partners/opengraph-image.tsx b/src/app/partners/opengraph-image.tsx new file mode 100644 index 000000000..5566a6188 --- /dev/null +++ b/src/app/partners/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "OpenChainBench partners and integrations. Projects that embed or reference our open benchmark data."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Partners", + headline: "Partners and integrations.", + subline: + "Projects and documentation sites that embed or reference OpenChainBench benchmark data in their products.", + }); +} diff --git a/src/app/perps/opengraph-image.tsx b/src/app/perps/opengraph-image.tsx new file mode 100644 index 000000000..186d421ee --- /dev/null +++ b/src/app/perps/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "Best perp DEX 2026. Live leaderboard of perpetual exchanges by volume, open interest, fees and funding."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Perp DEX leaderboard", + headline: "Best perp DEX, by the numbers.", + subline: + "Volume, open interest, all-in cost and funding rate across every major perpetual exchange. Updated continuously.", + }); +} diff --git a/src/app/prediction-markets/opengraph-image.tsx b/src/app/prediction-markets/opengraph-image.tsx new file mode 100644 index 000000000..b82b9a441 --- /dev/null +++ b/src/app/prediction-markets/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "Prediction markets leaderboard 2026. Volume, resolution delay, API latency and data freshness across every major venue."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Prediction markets", + headline: "Prediction markets, ranked live.", + subline: + "Volume, resolution delay, API latency and data freshness across Polymarket, Kalshi and every other major venue.", + }); +} diff --git a/src/app/reports/opengraph-image.tsx b/src/app/reports/opengraph-image.tsx new file mode 100644 index 000000000..c43dbce94 --- /dev/null +++ b/src/app/reports/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "OpenChainBench reports. In-depth analysis of crypto infrastructure performance backed by live benchmark data."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Reports", + headline: "Deeper dives, same data.", + subline: + "In-depth analysis of crypto infrastructure performance, backed by live benchmark data and open methodology.", + }); +} diff --git a/src/app/rpc/opengraph-image.tsx b/src/app/rpc/opengraph-image.tsx new file mode 100644 index 000000000..6cddfe973 --- /dev/null +++ b/src/app/rpc/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "Fastest RPC providers 2026, by chain and region. Live p50/p90/p99 latency and success rate."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "RPC benchmarks", + headline: "Fastest RPC, by chain and region.", + subline: + "p50/p90/p99 latency and success rate for every major RPC provider, probed every 60 seconds from US East, EU West and Singapore.", + }); +} diff --git a/src/app/team/opengraph-image.tsx b/src/app/team/opengraph-image.tsx new file mode 100644 index 000000000..5103abffc --- /dev/null +++ b/src/app/team/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "OpenChainBench team. The people building open benchmarks for crypto infrastructure."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Team", + headline: "The people behind the bench.", + subline: + "Building open, reproducible benchmarks for crypto infrastructure. Community-run, no vendor funding.", + }); +} diff --git a/src/app/trading-apps/opengraph-image.tsx b/src/app/trading-apps/opengraph-image.tsx new file mode 100644 index 000000000..323b069c6 --- /dev/null +++ b/src/app/trading-apps/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; + +export const runtime = "nodejs"; +export const alt = "Best Solana trading apps 2026. Live leaderboard ranked by volume, wallets, fees and app store ratings."; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function OG() { + return renderHubOG({ + kicker: "Trading apps", + headline: "Best Solana trading app, live.", + subline: + "Volume, active wallets, fees and app store ratings across every major Solana trading venue. Benchmarks updated continuously.", + }); +} From 48a261c2c7ed91e66773fadf7d65147a58453933 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:19:27 +0200 Subject: [PATCH 38/66] feat: add OG image cards for dynamic routes --- src/app/answers/[slug]/opengraph-image.tsx | 28 ++++ .../[slug]/[chain]/opengraph-image.tsx | 127 ++++++++++++++++++ .../category/[cat]/opengraph-image.tsx | 19 +++ .../[venueB]/[wallet]/opengraph-image.tsx | 27 ++++ .../hyperliquid/[slug]/opengraph-image.tsx | 25 ++++ src/app/perp/[slug]/opengraph-image.tsx | 20 +++ .../reports/[category]/opengraph-image.tsx | 19 +++ 7 files changed, 265 insertions(+) create mode 100644 src/app/answers/[slug]/opengraph-image.tsx create mode 100644 src/app/benchmarks/[slug]/[chain]/opengraph-image.tsx create mode 100644 src/app/benchmarks/category/[cat]/opengraph-image.tsx create mode 100644 src/app/fee-compare/[venueA]/[venueB]/[wallet]/opengraph-image.tsx create mode 100644 src/app/hyperliquid/[slug]/opengraph-image.tsx create mode 100644 src/app/perp/[slug]/opengraph-image.tsx create mode 100644 src/app/reports/[category]/opengraph-image.tsx diff --git a/src/app/answers/[slug]/opengraph-image.tsx b/src/app/answers/[slug]/opengraph-image.tsx new file mode 100644 index 000000000..3e6b77dba --- /dev/null +++ b/src/app/answers/[slug]/opengraph-image.tsx @@ -0,0 +1,28 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; +import { loadAllAnswers } from "@/lib/answers"; + +export const runtime = "nodejs"; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default async function OG({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const all = await loadAllAnswers(); + const answer = all.find((a) => a.slug === slug); + + if (!answer) { + return renderHubOG({ + kicker: "Answers", + headline: "Questions, answered with data.", + subline: "Common questions about crypto infrastructure, answered from live OpenChainBench benchmarks.", + }); + } + + const question = answer.question.endsWith("?") ? answer.question : `${answer.question}?`; + + return renderHubOG({ + kicker: "Answered by data", + headline: question.length > 60 ? question.slice(0, 57) + "..." : question, + subline: answer.seo_description ?? answer.short_answer.slice(0, 120), + }); +} diff --git a/src/app/benchmarks/[slug]/[chain]/opengraph-image.tsx b/src/app/benchmarks/[slug]/[chain]/opengraph-image.tsx new file mode 100644 index 000000000..f28275fdb --- /dev/null +++ b/src/app/benchmarks/[slug]/[chain]/opengraph-image.tsx @@ -0,0 +1,127 @@ +import { ImageResponse } from "next/og"; +import { getBenchmark } from "@/data/benchmarks"; +import { headlineSentence, leader } from "@/lib/citation"; +import { fmtUnit } from "@/lib/format"; +import { CATEGORY_COLOR } from "@/lib/category-colors"; +import { loadBenchmark } from "@/lib/spec"; +import { matchesChainSlug } from "@/lib/chain-aliases"; + +export const runtime = "nodejs"; +export const alt = "OpenChainBench. Open benchmarks for crypto infrastructure"; +export const size = { width: 1200, height: 630 }; +export const contentType = "image/png"; + +export default async function OG({ + params, +}: { + params: Promise<{ slug: string; chain: string }>; +}) { + const { slug, chain } = await params; + const b = + (await loadBenchmark(slug, { chain })) ?? (await getBenchmark(slug)); + if (!b) return new ImageResponse(
, { ...size }); + + const chainEntry = b.dimensions?.chain?.find((c) => + matchesChainSlug(c.value, chain), + ); + const chainLabel = chainEntry?.label ?? chain; + const top = leader(b); + const headline = top + ? `${top.name} leads at ${fmtUnit(top.value, b.unit)}` + : "Awaiting first run"; + const sentence = headlineSentence(b); + const catColor = CATEGORY_COLOR[b.category] ?? "#7a2e1f"; + const titleText = `${b.title} on ${chainLabel}`; + + return new ImageResponse( + ( +
+
+ OpenChainBench · Bench {b.number} + {b.category} +
+ +
+
+ {b.metric} · {chainLabel} +
+
38 ? 72 : 88, + fontWeight: 700, + lineHeight: 0.98, + letterSpacing: -2, + maxWidth: 1080, + }} + > + {titleText} +
+
+ {top ? sentence : b.subtitle} +
+
+ +
+ openchainbench.com/benchmarks/{b.slug}/{chain} + {headline} +
+
+ ), + { ...size }, + ); +} diff --git a/src/app/benchmarks/category/[cat]/opengraph-image.tsx b/src/app/benchmarks/category/[cat]/opengraph-image.tsx new file mode 100644 index 000000000..6376e2cfa --- /dev/null +++ b/src/app/benchmarks/category/[cat]/opengraph-image.tsx @@ -0,0 +1,19 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; +import { CATEGORY_BY_SLUG } from "@/lib/categories"; + +export const runtime = "nodejs"; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default async function OG({ params }: { params: Promise<{ cat: string }> }) { + const { cat } = await params; + const entry = CATEGORY_BY_SLUG.get(cat); + const label = entry?.heading ?? cat; + const description = entry?.description ?? "Live benchmarks across crypto infrastructure providers."; + + return renderHubOG({ + kicker: `${label} benchmarks`, + headline: `All ${label} benchmarks.`, + subline: description, + }); +} diff --git a/src/app/fee-compare/[venueA]/[venueB]/[wallet]/opengraph-image.tsx b/src/app/fee-compare/[venueA]/[venueB]/[wallet]/opengraph-image.tsx new file mode 100644 index 000000000..342de00bc --- /dev/null +++ b/src/app/fee-compare/[venueA]/[venueB]/[wallet]/opengraph-image.tsx @@ -0,0 +1,27 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; +import { PERP_VENUES } from "@/lib/perp-stats"; + +export const runtime = "nodejs"; +export const size = OG_SIZE; +export const contentType = "image/png"; + +function venueName(slug: string): string { + const cohortSlug = slug === "gmx" ? "gmx-v2" : slug; + return PERP_VENUES.find((v) => v.slug === cohortSlug)?.name ?? slug; +} + +export default async function OG({ + params, +}: { + params: Promise<{ venueA: string; venueB: string; wallet: string }>; +}) { + const { venueA, venueB } = await params; + const nameA = venueName(venueA); + const nameB = venueName(venueB); + + return renderHubOG({ + kicker: "Fee compare", + headline: `${nameA} vs ${nameB}.`, + subline: `Real taker fees paid on ${nameA} vs what they would have cost on ${nameB}. Live on-chain wallet data, no API key.`, + }); +} diff --git a/src/app/hyperliquid/[slug]/opengraph-image.tsx b/src/app/hyperliquid/[slug]/opengraph-image.tsx new file mode 100644 index 000000000..18d26ae8b --- /dev/null +++ b/src/app/hyperliquid/[slug]/opengraph-image.tsx @@ -0,0 +1,25 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; +import { fetchHlCohort } from "@/lib/hl-builder-stats"; + +export const runtime = "nodejs"; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default async function OG({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + + let name = slug; + try { + const cohort = await fetchHlCohort(); + const row = cohort?.rows.find((r) => r.slug === slug); + if (row?.name) name = row.name; + } catch { + // fall back to slug-derived name + } + + return renderHubOG({ + kicker: "Hyperliquid frontend", + headline: `${name}.`, + subline: `Revenue, volume and daily users for the ${name} Hyperliquid frontend. Live data from a local HL node tailing every fill.`, + }); +} diff --git a/src/app/perp/[slug]/opengraph-image.tsx b/src/app/perp/[slug]/opengraph-image.tsx new file mode 100644 index 000000000..d0f927e0b --- /dev/null +++ b/src/app/perp/[slug]/opengraph-image.tsx @@ -0,0 +1,20 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; +import { PERP_VENUES } from "@/lib/perp-stats"; + +export const runtime = "nodejs"; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default async function OG({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const cohortSlug = slug === "gmx" ? "gmx-v2" : slug; + const venue = PERP_VENUES.find((v) => v.slug === cohortSlug); + const name = venue?.name ?? slug; + const chain = venue?.chain ?? ""; + + return renderHubOG({ + kicker: "Perp DEX benchmark", + headline: `${name} benchmark.`, + subline: `Live volume, open interest, all-in fees and funding rate for ${name}${chain ? ` on ${chain}` : ""}. Compared against every major perp exchange.`, + }); +} diff --git a/src/app/reports/[category]/opengraph-image.tsx b/src/app/reports/[category]/opengraph-image.tsx new file mode 100644 index 000000000..ebec3508b --- /dev/null +++ b/src/app/reports/[category]/opengraph-image.tsx @@ -0,0 +1,19 @@ +import { OG_SIZE, renderHubOG } from "@/lib/og-hub-template"; +import { REPORT_CATEGORY_META } from "@/lib/reports/loader"; + +export const runtime = "nodejs"; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default async function OG({ params }: { params: Promise<{ category: string }> }) { + const { category } = await params; + const meta = REPORT_CATEGORY_META[category]; + const label = meta?.label ?? category; + const description = meta?.description ?? "In-depth analysis backed by live OpenChainBench benchmark data."; + + return renderHubOG({ + kicker: `${label} reports`, + headline: `${label} reports.`, + subline: description, + }); +} From 3d08c4449b9653193b6ecd44acacde331908ea8d Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:52:11 +0200 Subject: [PATCH 39/66] perf(seo): cache OG renders, SSR home ticker snapshot, fee-compare JSON-LD --- src/app/api/og/[slug]/route.tsx | 13 +++++- src/app/benchmarks/[slug]/opengraph-image.tsx | 2 +- src/app/benchmarks/[slug]/twitter-image.tsx | 2 +- src/app/fee-compare/page.tsx | 32 +++++++++++++++ src/app/page.tsx | 41 ++++++++++++++++++- .../[category]/[slug]/opengraph-image.tsx | 2 +- src/components/live/dashboard.tsx | 12 +++++- 7 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src/app/api/og/[slug]/route.tsx b/src/app/api/og/[slug]/route.tsx index d24490715..3a8cae4ff 100644 --- a/src/app/api/og/[slug]/route.tsx +++ b/src/app/api/og/[slug]/route.tsx @@ -129,7 +129,18 @@ export async function GET(
), - size, + { + ...size, + // Every social/AI unfurl used to trigger a full satori render + // (max-age=0 → x-vercel-cache MISS on consecutive GETs). The + // underlying data moves every 60s but a 1h-old card is fine for + // an unfurl; s-maxage bounds renders to ~24/day/slug while SWR + // keeps scrapes instant. + headers: { + "cache-control": + "public, s-maxage=3600, stale-while-revalidate=86400", + }, + }, ); } diff --git a/src/app/benchmarks/[slug]/opengraph-image.tsx b/src/app/benchmarks/[slug]/opengraph-image.tsx index 4b50596e4..c46b72f75 100644 --- a/src/app/benchmarks/[slug]/opengraph-image.tsx +++ b/src/app/benchmarks/[slug]/opengraph-image.tsx @@ -153,6 +153,6 @@ export default async function OG({
), - { ...size } + { ...size, headers: { "cache-control": "public, s-maxage=3600, stale-while-revalidate=86400" } } ); } diff --git a/src/app/benchmarks/[slug]/twitter-image.tsx b/src/app/benchmarks/[slug]/twitter-image.tsx index f720ed0e4..88fec66d2 100644 --- a/src/app/benchmarks/[slug]/twitter-image.tsx +++ b/src/app/benchmarks/[slug]/twitter-image.tsx @@ -162,6 +162,6 @@ export default async function TwitterImage({ ), - { ...size } + { ...size, headers: { "cache-control": "public, s-maxage=3600, stale-while-revalidate=86400" } } ); } diff --git a/src/app/fee-compare/page.tsx b/src/app/fee-compare/page.tsx index 3cc0f946f..7baf32889 100644 --- a/src/app/fee-compare/page.tsx +++ b/src/app/fee-compare/page.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { pageMetadata } from "@/lib/page-metadata"; import { FeeCompareClient } from "@/components/fee-compare-client"; +import { buildBreadcrumbJsonLd, safeJsonLd } from "@/lib/jsonld"; +import { SITE } from "@/data/site"; export const metadata: Metadata = pageMetadata({ path: "/fee-compare", @@ -26,8 +28,38 @@ export default async function FeeComparePage({ const rawDays = parseInt(params.days ?? "90", 10); const initialDays = isFinite(rawDays) ? Math.min(180, Math.max(7, rawDays)) : 90; + // Only page on the site without JSON-LD until 2026-09: emit the same + // BreadcrumbList shape every hub page ships, plus a WebApplication + // node describing the comparison tool itself. + const jsonLd = { + "@context": "https://schema.org", + "@graph": [ + buildBreadcrumbJsonLd([ + { name: "Home", item: SITE.url }, + { name: "Perp DEX fee comparison", item: `${SITE.url}/fee-compare` }, + ]), + { + "@type": "WebApplication", + "@id": `${SITE.url}/fee-compare#app`, + name: "Perp DEX fee comparison", + url: `${SITE.url}/fee-compare`, + applicationCategory: "FinanceApplication", + operatingSystem: "Web", + offers: { "@type": "Offer", price: "0", priceCurrency: "USD" }, + description: + "Compare taker fees between any two perp DEXs. Paste a wallet to see what was actually paid on Hyperliquid or Gains and what the same trades would have cost elsewhere.", + publisher: { "@id": `${SITE.url}/#org` }, + }, + ], + }; + return (
+