diff --git a/alternatives/bitquery.yml b/alternatives/bitquery.yml
index 9178d1e4..b0e77236 100644
--- a/alternatives/bitquery.yml
+++ b/alternatives/bitquery.yml
@@ -2,7 +2,7 @@ slug: bitquery
target_product: Bitquery
target_url: https://bitquery.io
description: GraphQL API for blockchain data, multi-chain
-benchmark: network-coverage
+benchmark: asset-registry-coverage
intro: |
Bitquery is a GraphQL surface on top of indexed onchain data, with the schema as the actual product: queries declare exactly the fields needed across transfers, trades, DEX events and account state, and the indexer fans those out across chains. Pricing is point-based, with each field, filter and join consuming from a monthly point budget rather than from a flat request quota, which means a single dashboard with many panels can hit the ceiling well before request volume looks high. Chain coverage is broad but uneven in depth: some networks expose full DEX trade indexing, others only basic transfers. The team usually compared next is the one whose chain list either covers a long-tail network Bitquery does not, or covers the same network with a flatter pricing model that does not penalise wide GraphQL fragments on a per-field basis.
diff --git a/alternatives/coingecko.yml b/alternatives/coingecko.yml
index 3b056eb6..40ecb3ec 100644
--- a/alternatives/coingecko.yml
+++ b/alternatives/coingecko.yml
@@ -2,7 +2,7 @@ slug: coingecko
target_product: CoinGecko
target_url: https://www.coingecko.com/en/api
description: Crypto price + market data API, multi-chain
-benchmark: network-coverage
+benchmark: asset-registry-coverage
intro: |
CoinGecko is a coin-centric price and market data REST API built around a curated asset list (the `/coins` universe) rather than around on-chain events. Tokens are added through a listing process, prices land on the API after that process completes, and the free tier is famously throttled (around 30 requests per minute on the demo key) which pushes any real product onto a paid plan within days of integration. The on-chain DEX side ships as a separate `/onchain` namespace inherited from GeckoTerminal, with its own quotas. Teams usually leave CoinGecko at one of two points: when the launch cadence of the tokens they care about outpaces the listing pipeline, or when the per-minute cap on the free tier forces a migration before the product is monetised enough to justify the enterprise tier.
diff --git a/alternatives/gains.yml b/alternatives/gains.yml
new file mode 100644
index 00000000..61dfe7cf
--- /dev/null
+++ b/alternatives/gains.yml
@@ -0,0 +1,16 @@
+slug: gains
+target_product: gains.trade
+target_url: https://gains.trade
+description: Oracle-priced perpetuals on Arbitrum, Base, Polygon and ApeChain
+benchmark: perp-fees
+
+intro: |
+ gains.trade v8 is an oracle-priced perpetuals venue running on Arbitrum, Base, Polygon and ApeChain. It settles against a GNS staking vault rather than a central orderbook, with the position open fee read directly on-chain from the fees contract and the spread term computed from half of the on-chain spreadP parameter. Because there is no orderbook, there is no price impact when executing against it: a $1M trade costs exactly the same number of basis points as a $1k trade. This benchmark measures the live all-in cost of opening a $1000 ETH long 10x position across major perpetual venues, every five minutes, by reading taker fees from each venue's own API and walking each orderbook for $1000 of buy-side notional. gains.trade is queried via eth_call on Base: pairs(N) gives the spreadP and feeIndex for each market, then fees(feeIndex) gives the open fee (totalPositionSizeFeeP). Values are in 1e10 precision per Gains v8 convention. The same harness queries Hyperliquid, Lighter, dYdX v4 and GMX v2. Funding rate is published separately because it accrues per hour held, not at open.
+
+seo_title: "gains.trade alternatives: live benchmark"
+seo_description: Compare gains.trade against Hyperliquid, Lighter, dYdX, GMX v2 on live taker fee plus spread crossed at $1k, $100k and $1M notional, refreshed every five minutes.
+
+status: live
+
+positioning: |
+ gains.trade v8 uses a decentralized GNS staking vault rather than an orderbook. Traders open positions against the vault, priced against a Chainlink or Pyth oracle rather than a live book. The vault absorbs trader profits and receives trader losses, with GNS stakers backstopping it in exchange for a share of protocol fees. This oracle-driven model is architecturally distinct from CLOB venues: there is no maker side, no maker rebate to earn, and the fee schedule is a single flat percentage of position size. For the perp-fees axis, this has one important implication: a $1M trade costs the same basis points as a $1k trade, because there is no book to walk and no price impact to eat. The all-in cost at any size equals the open fee plus half of spreadP, both read live from on-chain. The venue covers 364 active markets across crypto, forex, tokenized stocks, commodities and stock indices, the widest multi-asset catalog among EVM perp DEXes. The typical use case is directional traders on mid-cap or exotic pairs that lack deep orderbook liquidity on any CLOB venue; teams running large block trades also benefit from the flat cost structure versus a deep-but-not-infinite orderbook.
diff --git a/alternatives/the-graph.yml b/alternatives/the-graph.yml
index bc815819..8ec3883b 100644
--- a/alternatives/the-graph.yml
+++ b/alternatives/the-graph.yml
@@ -2,7 +2,7 @@ slug: the-graph
target_product: The Graph
target_url: https://thegraph.com
description: Decentralised indexing protocol with subgraph queries
-benchmark: network-coverage
+benchmark: asset-registry-coverage
intro: |
The Graph is a decentralised indexing protocol where each dataset (a subgraph) is written, deployed and queried independently rather than read off a pre-built schema. Queries are GQL against indexer nodes, billed in GRT, with a hosted gateway in front. The model means data shape is owned by whoever wrote the subgraph: a missing field, a stale index or a deprecated mapping is on the publisher, not on a central API team. Chain coverage tracks what indexers have chosen to support, which leans heavily EVM and lags on newer L2s and non-EVM networks until someone publishes a subgraph for them. The two reasons teams move off are the cost of running a private subgraph at production load, and the operational burden of debugging an indexer regression on a chain the public hosted service does not cover.
diff --git a/answers/which-prediction-market-data-api-is-the-freshest.yml b/answers/which-prediction-market-data-api-is-the-freshest.yml
deleted file mode 100644
index 856fe12d..00000000
--- a/answers/which-prediction-market-data-api-is-the-freshest.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-slug: which-prediction-market-data-api-is-the-freshest
-question: "Which prediction market data API publishes the freshest Polymarket data?"
-short_answer: |
- Polymarket's own CLOB WebSocket gateway is the canonical fastest source by definition (T0, sub-50ms from EU West) and is not a competing data provider. Among third-party data providers, Codex currently leads at {{p50:codex}} (p50, 24h) on the Polymarket tab, measured as lag from venue publish to provider relay; Codex indexes the onchain Polygon confirmation so its structural floor is roughly 2 seconds of block time. Live per-venue ranking at openchainbench.com/benchmarks/pm-data-freshness.
-
-benchmark: pm-data-freshness
-
-intro: |
- Prediction markets produce the most time sensitive event stream in crypto. An election market settles in seconds, a sports book moves on every play, and the price tick a trading UI shows is only as fresh as the API behind it. Builders integrating Polymarket through a data provider rather than hitting the CLOB directly need to know how many milliseconds that provider adds between Polymarket publishing a trade and the provider relaying the same trade to its WebSocket subscribers. This page answers exactly that. The OpenChainBench pm-data-freshness harness holds two WebSocket subscribers in parallel: Polymarket's own CLOB gateway (the canonical T0) and Codex GraphQL subscriptions, both subscribed to the same basket of top 20 Polymarket markets by 24 h volume, refreshed every 5 minutes. Each trade is matched across providers by a tuple of conditionId, price rounded to 3 decimals, trade size and a 5 second time bucket; the per provider lag versus Polymarket's gateway publish time is recorded as a Prometheus histogram and the leaderboard ranks by p50 in milliseconds, lower is fresher.
-
-methodology: |
- Two WebSocket clients run in parallel inside the harness on a Railway europe west 4 instance. Polymarket's own gateway (`wss://ws-subscriptions-clob.polymarket.com/ws/market`, public, no auth, sub 50 ms publish latency from EU West) is the canonical T0 by construction because nothing downstream can be faster than the source. Codex GraphQL (`wss://graph.codex.io/graphql` with the `graphql-transport-ws` subprotocol) runs an `onPredictionTradesCreated` firehose subscription filtered client side to the Polymarket protocol marketIds in the active basket. The cross correlation key is `(conditionId, priceUSD times 1000 rounded, sizeUSD times 1 million rounded, floor(trade_time / 5s))`; the 5 second bucket absorbs minor clock skew between gateways without merging unrelated trades. Histogram buckets run 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000 ms. Providers that fail to relay a matched trade within 90 seconds are not counted toward p50, only toward their receive total, so a provider cannot look fresh on the leaderboard by silently dropping events; the success rate column surfaces that.
-
-limitations:
- - "Polymarket's own gateway is the canonical T0 by construction. Its row on the leaderboard sits near zero because the harness compares everything else against it, not because Polymarket has solved network latency; the displayed sub millisecond value is a floor (vector(0.5)) so the row stays visible on the linear chart axis next to the multi second Codex value."
- - "Codex indexes the on chain Polygon confirmation of each Polymarket trade, so its p50 includes Polygon block time (around 2 seconds) plus ingestion. The Codex path is the right one for on chain reconciliation or settlement workflows; for live trading UIs the off chain relay path is the only viable one. The two answer different questions."
- - "Measurement is single region (Railway europe west 4). A provider whose edge gateway is closer to a US East client could post different deltas there; multi region requires running additional monitor instances of the freshness harness, which is on the roadmap."
- - "The basket is the top 20 active Polymarket markets by 24 h volume, refreshed every 5 minutes. Long tail markets with sparse trade flow are not on the leaderboard because the cross correlation matcher needs enough overlapping events to produce a stable histogram per provider."
- - "Kalshi and Limitless are separate exchanges with separate data feeds and a different ingestion path; this bench measures Polymarket as the underlying venue. A Kalshi specific tab is a candidate for a later phase but is not the current scope."
-
-faq:
- - q: "Which Polymarket data API has the lowest latency right now?"
- a: "{{best_name}} currently leads at {{best_p50}} (p50 over the last 24 hours), measured as wall clock time from Polymarket's own CLOB WebSocket publish to provider relay on the same trade. The leaderboard re sorts every minute on fresh Prometheus samples, so the answer reflects the actual measured lag on the active market basket, not a marketing claim. Codex's on chain Polygon indexer carries a structural floor near 2 seconds because it ingests block confirmations."
- - q: "What does freshness delta mean for a prediction market API?"
- a: "The bench connects to Polymarket's own CLOB WebSocket and to each provider's WebSocket simultaneously, subscribes to the same markets, and for every trade event records how many milliseconds the provider takes to relay the event after Polymarket itself publishes it. Lower is better. Polymarket's own gateway publish time is the canonical T0 because by construction nothing downstream can be faster than the source. The cross correlation key (conditionId, price, size, 5 s bucket) survives minor clock skew between gateways without merging unrelated trades."
- - q: "Why not include Polymarket REST polling on this bench?"
- a: "Freshness is a WebSocket question. REST polling at 1 second cadence would have a floor freshness near 500 ms (poll interval divided by 2) plus round trip time, dominated by how often you poll. The Polymarket gateway WebSocket exists for exactly this reason, to avoid that floor. Adding REST as a row would make the leaderboard noisy without changing the conclusion: WebSocket beats polling by definition for real time data."
- - q: "Are these numbers comparable to Kalshi or Limitless?"
- a: "Not directly. This benchmark measures Polymarket as the underlying venue since it is the largest and the one most providers proxy. Kalshi and Limitless are separate exchanges with separate data feeds, and providers that cover them often run a different ingestion path. A Kalshi specific tab is a candidate for a later phase. For now, treat the leaderboard as how fresh is your Polymarket data feed."
- - q: "How does the cross correlation matcher avoid false positives?"
- a: "The signature is (conditionId, priceUSD times 1000 rounded to 3 decimals, sizeUSD times 1 million rounded, floor(trade_time / 5 seconds)). Two unrelated trades would need to share the same conditionId, the same price to 3 decimals, the same size to 6 decimals, and land in the same 5 second window to collide. On the top 20 markets that combination is unique in practice; on a thinly traded long tail market it could in principle merge two trades, which is one reason the basket is restricted to the top 20 by volume."
- - q: "How often is the leaderboard refreshed?"
- a: "Continuously at the WebSocket level. Every trade event lands on the harness in real time and updates the freshness histogram; the page reads a rolling 24 h p50, p90 and p99 every minute. The market basket itself is refreshed every 5 minutes against `gamma-api.polymarket.com` so the leaderboard tracks current top markets rather than yesterday's hot list."
-
-related:
- - which-prediction-market-has-the-strictest-rate-limits
- - which-crypto-data-api-covers-the-most-blockchains
- - which-crypto-price-api-is-the-fastest
- - prediction-market-api-for-developers
-
-seo_title: "Which prediction market data API is the freshest in 2026?"
-seo_description: "Polymarket's own CLOB gateway is the canonical T0 (fastest by definition). Among third-party providers, Codex leads freshness delta live, measured by OpenChainBench on the top 20 Polymarket and Kalshi markets by volume."
-status: live
diff --git a/benchmarks/abstract-rpc.yml b/benchmarks/abstract-rpc.yml
new file mode 100644
index 00000000..d07924cd
--- /dev/null
+++ b/benchmarks/abstract-rpc.yml
@@ -0,0 +1,136 @@
+# OpenChainBench. Bench № 125
+
+slug: abstract-rpc
+number: "125"
+title: Fastest free Abstract RPC, live no-key EVM endpoint latency
+seo_title: "Fastest free Abstract RPC 2026"
+seo_description: "{{best_name}} leads free Abstract RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h). 3 no-key providers measured every 60s from 3 regions."
+subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public Abstract EVM endpoint, audited every 60 seconds from 3 regions.
+
+category: RPCs
+status: live
+metric: RPC latency
+unit: ms
+higher_is_better: false
+
+seo_intro: |
+ Abstract is a ZK Stack validium L2 (chain 2741) built by the Abstract Foundation, optimised for consumer applications and NFTs, ETH gas token, ~2 s block cadence, ZK proof settlement onto Ethereum. The 2026-08-02 audit confirms 3 clean keyless providers: the Abstract Foundation official endpoint (api.mainnet.abs.xyz), dRPC (abstract.drpc.org), and Thirdweb (abstract.rpc.thirdweb.com). Excluded: Ankr (no abstract route on public tier), Alchemy (key required), Infura (key required), QuickNode (key required).
+
+abstract: |
+ Per-chain member of the RPC latency cluster, extended to Abstract.
+ We measure the round-trip latency of a single, identical JSON-RPC call
+ (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public
+ Abstract endpoint that sustains continuous probing, 3 providers
+ at launch, every 60 seconds, from us-east, eu-west and Singapore. The
+ harness classifies every response (ok / http_err / jsonrpc_err / stale
+ / timeout) with an Abstract-scaled staleness gap (20 blocks
+ at Abstract's ~2 s block time). The cross-chain view
+ lives on the parent `rpc-capabilities` benchmark; this page is the
+ Abstract-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: `{\"jsonrpc\":\"2.0\",\"id\":,\"method\":\"eth_getBlockByNumber\",\"params\":[\"latest\", false]}`. Plain HTTP POST, identical for every endpoint, no API key in any request. Non-cacheable by design: the rotating id defeats edge caches that would answer a bare head query without touching a node."
+ - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours."
+ - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal."
+ - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/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=\"abstract\"`. Provider coverage at launch: 3 no-key endpoints."
+
+findings:
+ - "{{best_name}} currently leads free Abstract RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers."
+
+faq:
+ - q: "What is the fastest free Abstract RPC right now?"
+ a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment."
+ - q: "Which Abstract RPCs work without an API key?"
+ a: "3 endpoints sustain continuous keyless probing at launch: the Abstract Foundation official endpoint (api.mainnet.abs.xyz), dRPC (abstract.drpc.org), and Thirdweb (abstract.rpc.thirdweb.com). Every listed endpoint was live-verified with a `eth_getBlockByNumber` POST returning a parsable response before inclusion."
+ - q: "Does the fastest Abstract RPC change by region?"
+ a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate."
+ - q: "How is Abstract RPC latency measured here, technically?"
+ a: "One identical `eth_getBlockByNumber(\"latest\", false)` POST every 60 seconds against each provider from each of 3 regions, using the same plain HTTP client the rest of the RPC cluster uses. Wall-clock round-trip is recorded at millisecond precision; p50/p90/p99 are computed via Prometheus `quantile_over_time` over 24 hours. Responses are classified (`ok` / `http_err` / `jsonrpc_err` / `stale` / `timeout`) so an endpoint stuck on an old head or returning errors behind HTTP 200 is never ranked as fastest."
+
+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="abstract"})
+
+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: abstract-official
+ name: Abstract Foundation
+ tag: Abstract Foundation official public RPC
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Abstract Foundation's no-key abstract RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="abstract-official", chain="abstract"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="abstract-official", chain="abstract"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="abstract-official", chain="abstract"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="abstract-official", chain="abstract"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="abstract-official", chain="abstract"}) / sum(ocb:rpc_call:rate_24h{provider="abstract-official", chain="abstract"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="abstract-official", chain="abstract"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="abstract-official", chain="abstract"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="abstract-official", chain="abstract", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="abstract-official", chain="abstract", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="abstract-official", chain="abstract", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="abstract-official", chain="abstract", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="abstract-official", chain="abstract", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="abstract-official", chain="abstract", region="sgp"}[1h])
+
+ - slug: drpc
+ name: dRPC
+ tag: dRPC public gateway routing to Abstract node pool
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to dRPC's no-key abstract RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="abstract"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="abstract"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="abstract"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="abstract"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="abstract"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="abstract"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="abstract"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="abstract"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="abstract", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="abstract", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="abstract", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="abstract", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="abstract", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="abstract", region="sgp"}[1h])
+
+ - slug: thirdweb
+ name: Thirdweb
+ tag: Thirdweb public RPC gateway for Abstract
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Thirdweb's no-key abstract RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="abstract"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="thirdweb", chain="abstract"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="thirdweb", chain="abstract"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="thirdweb", chain="abstract"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="thirdweb", chain="abstract"}) / sum(ocb:rpc_call:rate_24h{provider="thirdweb", chain="abstract"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="thirdweb", chain="abstract"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="abstract"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="abstract", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="abstract", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="abstract", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="abstract", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="abstract", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="abstract", region="sgp"}[1h])
diff --git a/benchmarks/apechain-rpc.yml b/benchmarks/apechain-rpc.yml
new file mode 100644
index 00000000..c4e09ba4
--- /dev/null
+++ b/benchmarks/apechain-rpc.yml
@@ -0,0 +1,136 @@
+# OpenChainBench. Bench № 126
+
+slug: apechain-rpc
+number: "126"
+title: Fastest free ApeChain RPC, live no-key EVM endpoint latency
+seo_title: "Fastest free ApeChain RPC 2026"
+seo_description: "{{best_name}} leads free ApeChain RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h). 3 no-key providers measured every 60s from 3 regions."
+subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public ApeChain EVM endpoint, audited every 60 seconds from 3 regions.
+
+category: RPCs
+status: live
+metric: RPC latency
+unit: ms
+higher_is_better: false
+
+seo_intro: |
+ ApeChain is an Arbitrum Orbit L3 (chain 33139) operated by Yuga Labs / ApeDAO, dedicated to the APE ecosystem, APE gas token, ~250 ms block cadence, settlement onto Arbitrum One. The 2026-08-02 audit confirms 3 clean keyless providers: the ApeChain official endpoint (rpc.apechain.com/http), dRPC (apechain.drpc.org), and Thirdweb (apechain.rpc.thirdweb.com). Excluded: Alchemy (key required), QuickNode (key required), Infura (no apechain route).
+
+abstract: |
+ Per-chain member of the RPC latency cluster, extended to ApeChain.
+ We measure the round-trip latency of a single, identical JSON-RPC call
+ (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public
+ ApeChain endpoint that sustains continuous probing, 3 providers
+ at launch, every 60 seconds, from us-east, eu-west and Singapore. The
+ harness classifies every response (ok / http_err / jsonrpc_err / stale
+ / timeout) with an ApeChain-scaled staleness gap (20 blocks
+ at ApeChain's ~250 ms block time). The cross-chain view
+ lives on the parent `rpc-capabilities` benchmark; this page is the
+ ApeChain-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: `{\"jsonrpc\":\"2.0\",\"id\":,\"method\":\"eth_getBlockByNumber\",\"params\":[\"latest\", false]}`. Plain HTTP POST, identical for every endpoint, no API key in any request. Non-cacheable by design: the rotating id defeats edge caches that would answer a bare head query without touching a node."
+ - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours."
+ - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal."
+ - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/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=\"apechain\"`. Provider coverage at launch: 3 no-key endpoints."
+
+findings:
+ - "{{best_name}} currently leads free ApeChain RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers."
+
+faq:
+ - q: "What is the fastest free ApeChain RPC right now?"
+ a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment."
+ - q: "Which ApeChain RPCs work without an API key?"
+ a: "3 endpoints sustain continuous keyless probing at launch: the ApeChain official endpoint (rpc.apechain.com/http), dRPC (apechain.drpc.org), and Thirdweb (apechain.rpc.thirdweb.com). Every listed endpoint was live-verified with a `eth_getBlockByNumber` POST returning a parsable response before inclusion."
+ - q: "Does the fastest ApeChain RPC change by region?"
+ a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate."
+ - q: "How is ApeChain RPC latency measured here, technically?"
+ a: "One identical `eth_getBlockByNumber(\"latest\", false)` POST every 60 seconds against each provider from each of 3 regions, using the same plain HTTP client the rest of the RPC cluster uses. Wall-clock round-trip is recorded at millisecond precision; p50/p90/p99 are computed via Prometheus `quantile_over_time` over 24 hours. Responses are classified (`ok` / `http_err` / `jsonrpc_err` / `stale` / `timeout`) so an endpoint stuck on an old head or returning errors behind HTTP 200 is never ranked as fastest."
+
+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="apechain"})
+
+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: apechain-official
+ name: ApeChain
+ tag: ApeChain official public RPC
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to ApeChain's no-key apechain RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="apechain-official", chain="apechain"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="apechain-official", chain="apechain"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="apechain-official", chain="apechain"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="apechain-official", chain="apechain"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="apechain-official", chain="apechain"}) / sum(ocb:rpc_call:rate_24h{provider="apechain-official", chain="apechain"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="apechain-official", chain="apechain"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="apechain-official", chain="apechain"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="apechain-official", chain="apechain", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="apechain-official", chain="apechain", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="apechain-official", chain="apechain", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="apechain-official", chain="apechain", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="apechain-official", chain="apechain", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="apechain-official", chain="apechain", region="sgp"}[1h])
+
+ - slug: drpc
+ name: dRPC
+ tag: dRPC public gateway routing to ApeChain node pool
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to dRPC's no-key apechain RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="apechain"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="apechain"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="apechain"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="apechain"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="apechain"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="apechain"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="apechain"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="apechain"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="apechain", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="apechain", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="apechain", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="apechain", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="apechain", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="apechain", region="sgp"}[1h])
+
+ - slug: thirdweb
+ name: Thirdweb
+ tag: Thirdweb public RPC gateway for ApeChain
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Thirdweb's no-key apechain RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="apechain"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="thirdweb", chain="apechain"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="thirdweb", chain="apechain"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="thirdweb", chain="apechain"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="thirdweb", chain="apechain"}) / sum(ocb:rpc_call:rate_24h{provider="thirdweb", chain="apechain"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="thirdweb", chain="apechain"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="apechain"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="apechain", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="apechain", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="apechain", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="apechain", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="apechain", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="apechain", region="sgp"}[1h])
diff --git a/benchmarks/cyber-rpc.yml b/benchmarks/cyber-rpc.yml
new file mode 100644
index 00000000..09aa9296
--- /dev/null
+++ b/benchmarks/cyber-rpc.yml
@@ -0,0 +1,136 @@
+# OpenChainBench. Bench № 129
+
+slug: cyber-rpc
+number: "129"
+title: Fastest free Cyber RPC, live no-key EVM endpoint latency
+seo_title: "Fastest free Cyber Network RPC 2026"
+seo_description: "{{best_name}} leads free Cyber Network RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h). 3 no-key providers measured every 60s from 3 regions."
+subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public Cyber Network EVM endpoint, audited every 60 seconds from 3 regions.
+
+category: RPCs
+status: live
+metric: RPC latency
+unit: ms
+higher_is_better: false
+
+seo_intro: |
+ Cyber Network is an OP Stack L2 (chain 7560) operated by Cyber, a social-layer chain for SocialFi and Web3 social graphs, ETH gas token, ~2 s block cadence, blob calldata settlement on Ethereum. The 2026-08-02 audit confirms 3 clean keyless providers: Cyber official (rpc.cyber.co), AltLayer (cyber.alt.technology), and Thirdweb (7560.rpc.thirdweb.com). Excluded: dRPC (no cyber route), Blast API (no listing), Alchemy (key required).
+
+abstract: |
+ Per-chain member of the RPC latency cluster, extended to Cyber Network.
+ We measure the round-trip latency of a single, identical JSON-RPC call
+ (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public
+ Cyber Network endpoint that sustains continuous probing, 3 providers
+ at launch, every 60 seconds, from us-east, eu-west and Singapore. The
+ harness classifies every response (ok / http_err / jsonrpc_err / stale
+ / timeout) with a Cyber-scaled staleness gap (20 blocks
+ at Cyber's ~2 s block time). The cross-chain view
+ lives on the parent `rpc-capabilities` benchmark; this page is the
+ Cyber-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: `{\"jsonrpc\":\"2.0\",\"id\":,\"method\":\"eth_getBlockByNumber\",\"params\":[\"latest\", false]}`. Plain HTTP POST, identical for every endpoint, no API key in any request. Non-cacheable by design: the rotating id defeats edge caches that would answer a bare head query without touching a node."
+ - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours."
+ - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal."
+ - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/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=\"cyber\"`. Provider coverage at launch: 3 no-key endpoints."
+
+findings:
+ - "{{best_name}} currently leads free Cyber Network RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers."
+
+faq:
+ - q: "What is the fastest free Cyber Network RPC right now?"
+ a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment."
+ - q: "Which Cyber Network RPCs work without an API key?"
+ a: "3 endpoints sustain continuous keyless probing at launch: Cyber official (rpc.cyber.co), AltLayer (cyber.alt.technology), and Thirdweb (7560.rpc.thirdweb.com, chain-ID URL). Every listed endpoint was live-verified with a `eth_getBlockByNumber` POST returning a parsable response before inclusion."
+ - q: "Does the fastest Cyber Network RPC change by region?"
+ a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate."
+ - q: "How is Cyber Network RPC latency measured here, technically?"
+ a: "One identical `eth_getBlockByNumber(\"latest\", false)` POST every 60 seconds against each provider from each of 3 regions, using the same plain HTTP client the rest of the RPC cluster uses. Wall-clock round-trip is recorded at millisecond precision; p50/p90/p99 are computed via Prometheus `quantile_over_time` over 24 hours. Responses are classified (`ok` / `http_err` / `jsonrpc_err` / `stale` / `timeout`) so an endpoint stuck on an old head or returning errors behind HTTP 200 is never ranked as fastest."
+
+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="cyber"})
+
+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: cyber-official
+ name: Cyber Network
+ tag: Cyber Network official public RPC
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Cyber Network's no-key cyber RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="cyber-official", chain="cyber"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="cyber-official", chain="cyber"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="cyber-official", chain="cyber"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="cyber-official", chain="cyber"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="cyber-official", chain="cyber"}) / sum(ocb:rpc_call:rate_24h{provider="cyber-official", chain="cyber"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="cyber-official", chain="cyber"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="cyber-official", chain="cyber"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="cyber-official", chain="cyber", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="cyber-official", chain="cyber", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="cyber-official", chain="cyber", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="cyber-official", chain="cyber", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="cyber-official", chain="cyber", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="cyber-official", chain="cyber", region="sgp"}[1h])
+
+ - slug: altlayer
+ name: AltLayer
+ tag: AltLayer RPC node for Cyber Network
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to AltLayer's no-key cyber RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="altlayer", chain="cyber"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="altlayer", chain="cyber"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="altlayer", chain="cyber"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="altlayer", chain="cyber"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="altlayer", chain="cyber"}) / sum(ocb:rpc_call:rate_24h{provider="altlayer", chain="cyber"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="altlayer", chain="cyber"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="altlayer", chain="cyber"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="altlayer", chain="cyber", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="altlayer", chain="cyber", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="altlayer", chain="cyber", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="altlayer", chain="cyber", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="altlayer", chain="cyber", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="altlayer", chain="cyber", region="sgp"}[1h])
+
+ - slug: thirdweb
+ name: Thirdweb
+ tag: Thirdweb public RPC gateway for Cyber Network
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Thirdweb's no-key cyber RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="cyber"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="thirdweb", chain="cyber"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="thirdweb", chain="cyber"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="thirdweb", chain="cyber"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="thirdweb", chain="cyber"}) / sum(ocb:rpc_call:rate_24h{provider="thirdweb", chain="cyber"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="thirdweb", chain="cyber"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="cyber"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="cyber", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="cyber", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="cyber", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="cyber", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="cyber", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="cyber", region="sgp"}[1h])
diff --git a/benchmarks/dex-network-coverage.yml b/benchmarks/dex-network-coverage.yml
index 29b361bd..19852251 100644
--- a/benchmarks/dex-network-coverage.yml
+++ b/benchmarks/dex-network-coverage.yml
@@ -4,7 +4,7 @@ slug: dex-network-coverage
number: "090"
title: DEX indexer with most blockchains, live coverage ranking
seo_title: "DEX indexer most chains 2026"
-seo_description: "Which DEX indexer supports the most blockchains? Live coverage ranking across GeckoTerminal, Codex, DefiLlama, DexPaprika and Sim by Dune."
+seo_description: "Which DEX indexer supports the most blockchains? Live coverage ranking across GeckoTerminal, Codex, DexPaprika and Sim by Dune."
subtitle: Number of blockchains where each major DEX indexer actively indexes pools + swap volumes, audited every six hours against each provider's public networks endpoint.
category: Aggregators
status: live
@@ -20,29 +20,28 @@ seo_intro: |
can this API return pool addresses, live swap events, OHLCV candles
and trending pools. Providers on this leaderboard run their own DEX
pool indexer as a product (GeckoTerminal `/networks`, Codex
- `getNetworks`, DefiLlama `/chains`, DexPaprika `/networks`, Sim by
- Dune `/evm/supported-chains`). Asset-registry coverage (chains where
- an API knows tokens for contract-address lookup) is a different
- question measured on the sister bench asset-registry-coverage.
+ `getNetworks`, DexPaprika `/networks`, Sim by Dune
+ `/evm/supported-chains`). Asset-registry coverage (chains where an
+ API knows tokens for contract-address lookup) is a different question
+ measured on the sister bench asset-registry-coverage.
abstract: |
We benchmark how many networks each major DEX indexer publishes as
actively indexed for pools + swap volumes. The harness fetches the
- official listing every six hours from five providers (GeckoTerminal,
- Codex, DefiLlama, DexPaprika, Sim by Dune), deduplicates by chain id
- or platform slug and counts. Mainnet only. Asset-registry coverage
- (chains where the provider knows tokens for contract-address lookups)
- is measured on bench asset-registry-coverage because it answers a
- different question, an API can list tokens on 300 chains through a
- market-data pipeline while running DEX pool indexing on only a
- dozen. Both leaderboards live under Aggregators so a reader can
- compare a provider on the axis that matches their product.
+ official listing every six hours from four providers (GeckoTerminal,
+ Codex, DexPaprika, Sim by Dune), deduplicates by chain id or platform
+ slug and counts. Mainnet only. Asset-registry coverage (chains where
+ the provider knows tokens for contract-address lookups) is measured
+ on bench asset-registry-coverage because it answers a different
+ question, an API can list tokens on 300 chains through a market-data
+ pipeline while running DEX pool indexing on only a dozen. Both
+ leaderboards live under Aggregators so a reader can compare a
+ provider on the axis that matches their product.
methodology:
- "Source: each provider's public DEX-indexer networks endpoint."
- "GeckoTerminal: GET /api/v2/networks (paginated, no auth). Chains where GeckoTerminal actively indexes DEX pools + trades."
- "Codex: GraphQL `getNetworks` query at https://graph.codex.io/graphql with an official API key. Chains covered by Defined.fi's DEX data pipeline."
- - "DefiLlama: GET https://api.llama.fi/chains (no auth). Chains tracked by DefiLlama's TVL crawler, every chain with at least one indexed DeFi protocol, DEX chains are a subset."
- "DexPaprika: GET https://api.dexpaprika.com/networks (no auth). CoinPaprika's dedicated DEX tracker product, separate from the market-data API."
- "Sim by Dune: GET https://api.sim.dune.com/v1/evm/supported-chains (no auth). EVM only, mainnets filtered via the `mainnet` tag."
- "Cadence: full refresh every 6 hours."
@@ -53,7 +52,6 @@ findings:
- "{{best_name}} currently leads at {{best_p50}} chains across {{count}} measured providers. The count is read live from each provider's DEX-networks endpoint, so the number reflects where each API actively indexes DEX pools + swap volumes today, not a marketing claim."
- "{{name:geckoterminal}} sits at {{p50:geckoterminal}} chains via `/api/v2/networks`. GeckoTerminal indexes wherever CoinGecko's DEX pipeline reaches, so the count tracks the CoinGecko DEX-rollout roadmap rather than a standalone listing decision."
- "{{name:codex}} clocks {{p50:codex}} chains via `getNetworks`. Defined.fi historically prioritises depth on EVM + Solana DEX coverage rather than breadth across niche chains, which surfaces here as a tighter count with stronger per-chain DEX quality than a raw breadth ranking implies."
- - "{{name:defillama}} publishes {{p50:defillama}} chains via `/chains`. Broader than DEX-only (includes lending, LSTs, restaking, bridges) because the endpoint tracks every chain with at least one indexed DeFi protocol, DEX chains are a subset. The count is the canonical open reference for chains with meaningful onchain financial activity."
- "DEX-indexer breadth is one axis of a data API. Asset-registry breadth (bench asset-registry-coverage) and per-chain head lag (bench aggregator-head-lag) measure the other axes a serious DEX-integration decision needs."
source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/network-coverage
@@ -67,8 +65,6 @@ faq:
a: "{{best_name}} currently leads at {{best_p50}} chains across {{count}} measured providers. The count is read every six hours directly from each provider's own DEX-networks endpoint and deduplicated by chain id, so the leaderboard reflects where each API actively indexes DEX pools + swap volumes today, not a claim from a marketing page."
- q: "Why is this bench separate from asset-registry-coverage?"
a: "DEX-pool coverage and asset-registry coverage are two different products, not a single metric. GeckoTerminal indexes DEX pools on ~265 chains via `/networks`; CoinPaprika lists 300+ chains in `/v1/contracts` because it indexes token metadata everywhere. Comparing them on one leaderboard makes the number meaningless, a market-data API always wins on chain count while a DEX indexer always wins on pool-level depth. This bench answers the DEX-indexing question. The sister bench asset-registry-coverage answers the token-lookup question."
- - q: "Does DefiLlama really index DEX pools on 400+ chains?"
- a: "DefiLlama's `/chains` endpoint tracks every chain with at least one indexed DeFi protocol (DEXes, lending, LSTs, restaking, bridges, oracles), so the count is broader than DEX-only. DEX chains are a subset. DefiLlama sits on the leaderboard because the same crawler backs their DEX-volumes aggregator, and the endpoint is the canonical open reference for `chains with meaningful onchain financial activity`. Read alongside the more DEX-specific counts from GeckoTerminal, Codex and DexPaprika for the DEX-only view."
- q: "Why isn't CoinGecko / Mobula / CoinPaprika on this leaderboard?"
a: "Those providers are measured on the sister bench asset-registry-coverage because their product is chain-scoped token metadata + market data, not DEX pool indexing. CoinGecko powers GeckoTerminal on the DEX side, but the token-registry number and the DEX-indexed number are measured separately because they answer different questions. Mobula and CoinPaprika are market-data APIs, not DEX pool indexers, DexPaprika is CoinPaprika's dedicated DEX product and is on this leaderboard."
- q: "How often is the chain count refreshed?"
@@ -77,8 +73,7 @@ faq:
a: "Any chain a provider lists as a production network for DEX indexing, identified by a unique chain id or slug. Ethereum, Solana, Base, Arbitrum, BNB, Avalanche and so on each count once regardless of how the provider labels them internally. Testnet listings are filtered server-side where the provider exposes a testnet flag. Sim by Dune is EVM only by construction of the `/v1/evm/supported-chains` endpoint."
# Real metrics exposed by the network-coverage harness:
-# networks_supported_total{provider="geckoterminal"|"codex"|
-# "defillama"|"dexpaprika"|"dune"}
+# networks_supported_total{provider="geckoterminal"|"codex"|"dexpaprika"|"dune"}
# -> gauge, the unique-chain count from the latest successful refresh.
providers:
@@ -108,19 +103,6 @@ providers:
sample_size: networks_supported_total{provider="codex"}
series: networks_supported_total{provider="codex"}
- - slug: defillama
- name: DefiLlama
- tag: TVL + DEX-volumes aggregator
- formula: "Count of chains returned by DefiLlama's public `/chains` endpoint (chains with at least one indexed DeFi protocol; DEX chains are a subset), refreshed every 6 hours."
- queries:
- p50: networks_supported_total{provider="defillama"}
- p90: networks_supported_total{provider="defillama"}
- p99: networks_supported_total{provider="defillama"}
- mean: networks_supported_total{provider="defillama"}
- success: clamp_max(networks_supported_total{provider="defillama"} > bool 0, 1)
- sample_size: networks_supported_total{provider="defillama"}
- series: networks_supported_total{provider="defillama"}
-
- slug: dexpaprika
name: DexPaprika
tag: CoinPaprika's DEX product
diff --git a/benchmarks/kava-rpc.yml b/benchmarks/kava-rpc.yml
new file mode 100644
index 00000000..6fa77742
--- /dev/null
+++ b/benchmarks/kava-rpc.yml
@@ -0,0 +1,182 @@
+# OpenChainBench. Bench № 121
+
+slug: kava-rpc
+number: "121"
+title: Fastest free Kava RPC, live no-key EVM endpoint latency
+seo_title: "Fastest free Kava RPC 2026"
+seo_description: "{{best_name}} leads free Kava RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h). 5 no-key providers measured every 60s from 3 regions."
+subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public Kava EVM endpoint, audited every 60 seconds from 3 regions.
+
+category: RPCs
+status: live
+metric: RPC latency
+unit: ms
+higher_is_better: false
+
+seo_intro: |
+ Kava is a Cosmos SDK Layer 1 (chain 2222) with a native EVM execution layer, ~5 s block time under Tendermint BFT consensus, KAVA gas token on the EVM side, and IBC connectivity to the wider Cosmos ecosystem. The 2026-08-03 audit confirms 5 clean keyless providers: the Kava Labs official endpoint (evm.kava.io), dRPC (kava.drpc.org), Thirdweb (kava.rpc.thirdweb.com), PublicNode (kava-evm-rpc.publicnode.com), and Ankr (rpc.ankr.com/kava_evm). Excluded: Tenderly (no kava route), Blast API (no listing), OnFinality (rate-limited without a key), 1RPC (unknown network).
+
+abstract: |
+ Per-chain member of the RPC latency cluster, extended to Kava EVM.
+ We measure the round-trip latency of a single, identical JSON-RPC call
+ (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public
+ Kava EVM endpoint that sustains continuous probing, 5 providers
+ measured, every 60 seconds, from us-east, eu-west and Singapore. The
+ harness classifies every response (ok / http_err / jsonrpc_err / stale
+ / timeout) with a Kava-scaled staleness gap (20 blocks
+ at Kava's ~5 s block time). The cross-chain view
+ lives on the parent `rpc-capabilities` benchmark; this page is the
+ Kava-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: `{\"jsonrpc\":\"2.0\",\"id\":,\"method\":\"eth_getBlockByNumber\",\"params\":[\"latest\", false]}`. Plain HTTP POST, identical for every endpoint, no API key in any request. Non-cacheable by design: the rotating id defeats edge caches that would answer a bare head query without touching a node."
+ - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours."
+ - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal."
+ - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/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=\"kava\"`. Provider coverage: 5 no-key endpoints."
+
+findings:
+ - "{{best_name}} currently leads free Kava RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 5 measured providers."
+
+faq:
+ - q: "What is the fastest free Kava RPC right now?"
+ a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment."
+ - q: "Which Kava RPCs work without an API key?"
+ a: "5 endpoints sustain continuous keyless probing: the Kava Labs official endpoint (evm.kava.io), dRPC (kava.drpc.org), Thirdweb (kava.rpc.thirdweb.com), PublicNode (kava-evm-rpc.publicnode.com), and Ankr (rpc.ankr.com/kava_evm). Every listed endpoint was live-verified with a `eth_getBlockByNumber` POST returning a parsable response before inclusion."
+ - q: "Does the fastest Kava RPC change by region?"
+ a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate."
+ - q: "How is Kava RPC latency measured here, technically?"
+ a: "One identical `eth_getBlockByNumber(\"latest\", false)` POST every 60 seconds against each provider from each of 3 regions, using the same plain HTTP client the rest of the RPC cluster uses. Wall-clock round-trip is recorded at millisecond precision; p50/p90/p99 are computed via Prometheus `quantile_over_time` over 24 hours. Responses are classified (`ok` / `http_err` / `jsonrpc_err` / `stale` / `timeout`) so an endpoint stuck on an old head or returning errors behind HTTP 200 is never ranked as fastest."
+
+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="kava"})
+
+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: kava-official
+ name: Kava Labs
+ tag: Kava Labs official public EVM RPC
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Kava Labs's no-key kava EVM RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="kava-official", chain="kava"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="kava-official", chain="kava"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="kava-official", chain="kava"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="kava-official", chain="kava"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="kava-official", chain="kava"}) / sum(ocb:rpc_call:rate_24h{provider="kava-official", chain="kava"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="kava-official", chain="kava"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="kava-official", chain="kava"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="kava-official", chain="kava", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="kava-official", chain="kava", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="kava-official", chain="kava", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="kava-official", chain="kava", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="kava-official", chain="kava", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="kava-official", chain="kava", region="sgp"}[1h])
+
+ - slug: drpc
+ name: dRPC
+ tag: dRPC public gateway routing to Kava node pool
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to dRPC's no-key kava RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="kava"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="kava"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="kava"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="kava"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="kava"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="kava"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="kava"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="kava"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="kava", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="kava", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="kava", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="kava", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="kava", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="kava", region="sgp"}[1h])
+
+ - slug: thirdweb
+ name: Thirdweb
+ tag: Thirdweb public RPC gateway for Kava EVM
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Thirdweb's no-key kava RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="kava"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="thirdweb", chain="kava"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="thirdweb", chain="kava"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="thirdweb", chain="kava"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="thirdweb", chain="kava"}) / sum(ocb:rpc_call:rate_24h{provider="thirdweb", chain="kava"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="thirdweb", chain="kava"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="kava"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="kava", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="kava", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="kava", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="kava", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="kava", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="kava", region="sgp"}[1h])
+
+ - slug: publicnode
+ name: PublicNode
+ tag: PublicNode free public RPC for Kava EVM
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to PublicNode's no-key kava RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="kava"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="publicnode", chain="kava"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="publicnode", chain="kava"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="publicnode", chain="kava"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="publicnode", chain="kava"}) / sum(ocb:rpc_call:rate_24h{provider="publicnode", chain="kava"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="publicnode", chain="kava"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="kava"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="kava", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="kava", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="kava", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="kava", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="kava", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="kava", region="sgp"}[1h])
+
+ - slug: ankr
+ name: Ankr
+ tag: Ankr public RPC gateway for Kava EVM
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Ankr's no-key kava RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="kava"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="ankr", chain="kava"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="ankr", chain="kava"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="ankr", chain="kava"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="ankr", chain="kava"}) / sum(ocb:rpc_call:rate_24h{provider="ankr", chain="kava"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="ankr", chain="kava"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="kava"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="kava", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="kava", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="kava", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="kava", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="kava", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="kava", region="sgp"}[1h])
diff --git a/benchmarks/lisk-rpc.yml b/benchmarks/lisk-rpc.yml
new file mode 100644
index 00000000..8ed450ab
--- /dev/null
+++ b/benchmarks/lisk-rpc.yml
@@ -0,0 +1,136 @@
+# OpenChainBench. Bench № 127
+
+slug: lisk-rpc
+number: "127"
+title: Fastest free Lisk RPC, live no-key EVM endpoint latency
+seo_title: "Fastest free Lisk RPC 2026"
+seo_description: "{{best_name}} leads free Lisk RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h). 3 no-key providers measured every 60s from 3 regions."
+subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public Lisk EVM endpoint, audited every 60 seconds from 3 regions.
+
+category: RPCs
+status: live
+metric: RPC latency
+unit: ms
+higher_is_better: false
+
+seo_intro: |
+ Lisk is an OP Stack L2 (chain 1135) operated by Onchain Foundation (formerly Lisk Foundation), focused on emerging-market Web3 adoption, ETH gas token, ~2 s block cadence, blob calldata settlement on Ethereum. The 2026-08-02 audit confirms 3 clean keyless providers: the Lisk official endpoint (rpc.api.lisk.com), dRPC (lisk.drpc.org), and Thirdweb (lisk.rpc.thirdweb.com). Excluded: Blast API (no lisk route), Alchemy (key required), Infura (no lisk route), QuickNode (key required).
+
+abstract: |
+ Per-chain member of the RPC latency cluster, extended to Lisk.
+ We measure the round-trip latency of a single, identical JSON-RPC call
+ (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public
+ Lisk endpoint that sustains continuous probing, 3 providers
+ at launch, every 60 seconds, from us-east, eu-west and Singapore. The
+ harness classifies every response (ok / http_err / jsonrpc_err / stale
+ / timeout) with a Lisk-scaled staleness gap (20 blocks
+ at Lisk's ~2 s block time). The cross-chain view
+ lives on the parent `rpc-capabilities` benchmark; this page is the
+ Lisk-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: `{\"jsonrpc\":\"2.0\",\"id\":,\"method\":\"eth_getBlockByNumber\",\"params\":[\"latest\", false]}`. Plain HTTP POST, identical for every endpoint, no API key in any request. Non-cacheable by design: the rotating id defeats edge caches that would answer a bare head query without touching a node."
+ - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours."
+ - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal."
+ - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/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=\"lisk\"`. Provider coverage at launch: 3 no-key endpoints."
+
+findings:
+ - "{{best_name}} currently leads free Lisk RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers."
+
+faq:
+ - q: "What is the fastest free Lisk RPC right now?"
+ a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment."
+ - q: "Which Lisk RPCs work without an API key?"
+ a: "3 endpoints sustain continuous keyless probing at launch: the Lisk official endpoint (rpc.api.lisk.com), dRPC (lisk.drpc.org), and Thirdweb (lisk.rpc.thirdweb.com). Every listed endpoint was live-verified with a `eth_getBlockByNumber` POST returning a parsable response before inclusion."
+ - q: "Does the fastest Lisk RPC change by region?"
+ a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate."
+ - q: "How is Lisk RPC latency measured here, technically?"
+ a: "One identical `eth_getBlockByNumber(\"latest\", false)` POST every 60 seconds against each provider from each of 3 regions, using the same plain HTTP client the rest of the RPC cluster uses. Wall-clock round-trip is recorded at millisecond precision; p50/p90/p99 are computed via Prometheus `quantile_over_time` over 24 hours. Responses are classified (`ok` / `http_err` / `jsonrpc_err` / `stale` / `timeout`) so an endpoint stuck on an old head or returning errors behind HTTP 200 is never ranked as fastest."
+
+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="lisk"})
+
+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: lisk-official
+ name: Lisk
+ tag: Lisk official public RPC
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Lisk's no-key lisk RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lisk-official", chain="lisk"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="lisk-official", chain="lisk"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="lisk-official", chain="lisk"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="lisk-official", chain="lisk"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="lisk-official", chain="lisk"}) / sum(ocb:rpc_call:rate_24h{provider="lisk-official", chain="lisk"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="lisk-official", chain="lisk"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="lisk-official", chain="lisk"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lisk-official", chain="lisk", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="lisk-official", chain="lisk", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lisk-official", chain="lisk", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="lisk-official", chain="lisk", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lisk-official", chain="lisk", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="lisk-official", chain="lisk", region="sgp"}[1h])
+
+ - slug: drpc
+ name: dRPC
+ tag: dRPC public gateway routing to Lisk node pool
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to dRPC's no-key lisk RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="lisk"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="lisk"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="lisk"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="lisk"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="lisk"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="lisk"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="lisk"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="lisk"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="lisk", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="lisk", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="lisk", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="lisk", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="lisk", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="lisk", region="sgp"}[1h])
+
+ - slug: thirdweb
+ name: Thirdweb
+ tag: Thirdweb public RPC gateway for Lisk
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Thirdweb's no-key lisk RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="lisk"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="thirdweb", chain="lisk"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="thirdweb", chain="lisk"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="thirdweb", chain="lisk"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="thirdweb", chain="lisk"}) / sum(ocb:rpc_call:rate_24h{provider="thirdweb", chain="lisk"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="thirdweb", chain="lisk"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="lisk"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="lisk", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="lisk", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="lisk", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="lisk", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="lisk", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="lisk", region="sgp"}[1h])
diff --git a/benchmarks/perp-active-markets.yml b/benchmarks/perp-active-markets.yml
new file mode 100644
index 00000000..f3a93b90
--- /dev/null
+++ b/benchmarks/perp-active-markets.yml
@@ -0,0 +1,268 @@
+# OpenChainBench. Bench 116
+
+slug: perp-active-markets
+number: "116"
+title: Most tradable markets on a perp DEX, live count across 14 venues
+seo_title: "Perp DEX market count 2026"
+seo_description: "Live count of active perpetual futures markets across 14 DEX venues: Aster, gains.trade, edgeX, Extended, Aevo, Lighter, Hyperliquid, GRVT, dYdX, Ostium, Pacifica, Paradex, Vertex, Polymarket. Refreshed every 5 minutes."
+subtitle: Live count of active perpetual futures markets per venue, polled every 5 minutes from each venue's public API. Higher means more tradable instruments across crypto, forex, stocks, indices and commodities.
+category: Trading
+status: live
+metric: Active markets
+unit: count
+higher_is_better: true
+
+seo_intro: |
+ This benchmark ranks 14 major onchain perp venues by total active markets.
+ Market count is the fastest proxy for breadth: a venue with 364 pairs gives
+ traders access to crypto, forex (EUR/USD, GBP/USD), tokenized stocks, commodities
+ and stock indices alongside BTC and ETH. We poll each venue's public API every
+ 5 minutes and publish one gauge per venue. gains.trade lists 364 pairs, the
+ widest catalog among EVM perp DEXes, vs Hyperliquid at 177 and dYdX at 114.
+
+abstract: |
+ The bench polls 14 perp DEX venues every 5 minutes and publishes a live count
+ of active markets per venue. Sources: native API for most venues (Hyperliquid
+ info metaAndAssetCtxs, Lighter orderBookDetails with market_type==perp filter,
+ dYdX v4 indexer perpetualMarkets, Paradex markets summary, edgeX native markets,
+ Aster fapi exchangeInfo PERPETUAL instruments, Vertex indexer, GRVT markets,
+ Extended public markets, Aevo markets, Pacifica Solana program state, Ostium
+ subgraph, Polymarket perps tickers). gains.trade uses the Mobula perp pairs
+ catalog (dex=gains, mainnet chains only, arbitrum-sepolia excluded), the same
+ source used by the perp-cohort-stats harness for all active_markets signals.
+ Higher is better.
+
+methodology:
+ - "Cadence: every 5 minutes per venue in parallel, 10 second timeout per request."
+ - "Hyperliquid: info metaAndAssetCtxs, count of non-delisted assets. Active perp markets only, crypto assets."
+ - "Lighter: orderBookDetails, count of markets where market_type==perp and status==active. Native API, mainnet only."
+ - "dYdX v4: indexer perpetualMarkets, count of non-deactivated markets."
+ - "Paradex: markets summary endpoint on Starknet L2, count of active perp markets."
+ - "edgeX: native markets endpoint, count of active perp markets on zkSync."
+ - "Aster: fapi exchangeInfo, count of instruments with contractType==PERPETUAL and status==TRADING on BNB Chain."
+ - "Vertex: public indexer, count of active perp products on Arbitrum."
+ - "GRVT: public markets endpoint, count of active instruments on ZK Stack."
+ - "Extended: public markets API, count of active perp markets on StarkEx."
+ - "Aevo: public markets endpoint, count of active perp instruments on OP Stack."
+ - "Pacifica: onchain reads on the Solana program state, count of active perp markets."
+ - "Ostium: Arbitrum subgraph, count of active pairs. Covers FX and commodities alongside crypto."
+ - "Polymarket: public perps tickers endpoint on Polygon, count of listed instruments."
+ - "gains.trade: Mobula perp pairs catalog (GET /api/2/perp/pairs, dex=gains, mainnet only). Covers crypto, forex, stocks, indices and commodities across Arbitrum, Base, Polygon and ApeChain."
+ - "Headline: avg_over_time of the live market count gauge over the last 24 hours. Listings change infrequently so the 24h average is effectively the current count."
+ - "Failures: a venue that errors keeps its last gauge value and its perp_venue_health gauge drops toward 0; the leaderboard tags it as stale."
+
+findings:
+ - "{{best_name}} leads the cohort with {{best_p50}} active markets across 14 measured perp DEX venues."
+ - "{{name:gains}} lists {{p50:gains}} active markets, spanning crypto, forex, stocks, indices and commodities. The widest multi-asset catalog among EVM perp DEXes."
+ - "{{name:aster}} lists {{p50:aster}} active markets. BNB Chain perps with a Binance-compatible API."
+ - "{{name:edgex}} lists {{p50:edgex}} active markets. zkSync-based offchain CLOB."
+ - "{{name:extended}} lists {{p50:extended}} active markets. StarkEx-based perps venue."
+ - "{{name:aevo}} lists {{p50:aevo}} active markets. Optimism L2 orderbook, options-focused history."
+ - "{{name:lighter}} lists {{p50:lighter}} active markets. Zero taker fee onchain orderbook on zkSync."
+ - "{{name:hyperliquid}} lists {{p50:hyperliquid}} active markets. HyperBFT L1 orderbook, crypto assets only."
+ - "{{name:grvt}} lists {{p50:grvt}} active markets. Hybrid CEX/DEX perps on ZK Stack."
+ - "{{name:dydx}} lists {{p50:dydx}} active markets. Cosmos appchain orderbook, one of the oldest decentralized perp venues."
+ - "{{name:ostium}} lists {{p50:ostium}} active markets. Synthetic perps on Arbitrum, FX and commodities alongside crypto."
+ - "{{name:pacifica}} lists {{p50:pacifica}} active markets. Solana-native onchain perps."
+ - "{{name:paradex}} lists {{p50:paradex}} active markets. Starknet L2 perps."
+ - "{{name:vertex}} lists {{p50:vertex}} active markets. Arbitrum orderbook plus AMM hybrid."
+ - "{{name:polymarket}} lists {{p50:polymarket}} active markets. Polygon perps, launched 2026-07."
+
+source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/perp-cohort-stats
+
+prometheus:
+ window: 24h
+ expected_freshness_seconds: 1800
+
+faq:
+ - q: "What does this benchmark measure?"
+ a: "The live count of active perpetual futures markets per venue. A market is a tradable instrument with a public price feed. Higher is better: more markets means more choice for traders."
+ - q: "Why does gains.trade use Mobula as a source instead of its native API?"
+ a: "The perp-cohort-stats harness uses the Mobula perp pairs catalog for gains because Mobula curates the full market taxonomy with asset-class tags. The count is mainnet-only (testnet chains excluded) and matches the inventory gains.trade exposes on its interface."
+ - q: "Is market count a good proxy for venue breadth?"
+ a: "It is the fastest proxy. A venue with 364 markets covering crypto, forex, stocks and commodities gives traders far more choice than one with 177 crypto-only markets. Depth per market is a separate signal; pair this bench with perp-open-interest and perp-fees for the full picture."
+ - q: "Why is Aster first if gains.trade covers more asset classes?"
+ a: "Aster lists more raw instruments (all USDT-margined pairs on BNB Chain including many micro-cap crypto assets). gains.trade lists fewer instruments but across a broader set of asset classes. The bench ranks by raw count; the findings text calls out gains as the widest multi-asset catalog among EVM DEXes."
+
+providers:
+ - slug: aster
+ name: Aster
+ tag: BNB Chain perps DEX, Binance-compatible API
+ formula: "Count of PERPETUAL instruments with status==TRADING from Aster fapi exchangeInfo. USDT-margined crypto pairs on BNB Chain."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="aster"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="aster"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="aster"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="aster"}[24h])
+ success: avg_over_time(perp_venue_health{venue="aster"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="aster"}[24h])
+ series: perp_venue_active_markets{venue="aster"}
+
+ - slug: gains
+ name: gains.trade
+ tag: Synthetic perps on Arbitrum, Base, Polygon and ApeChain, multi-asset
+ formula: "Count of active mainnet perp pairs from Mobula's catalog (dex=gains, testnet excluded). Covers crypto, forex, stocks, indices and commodities."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="gains"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="gains"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="gains"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="gains"}[24h])
+ success: avg_over_time(perp_venue_health{venue="gains"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="gains"}[24h])
+ series: perp_venue_active_markets{venue="gains"}
+
+ - slug: edgex
+ name: edgeX
+ tag: zkSync-based offchain CLOB
+ formula: "Count of active perp markets from edgeX native markets endpoint on zkSync."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="edgex"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="edgex"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="edgex"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="edgex"}[24h])
+ success: avg_over_time(perp_venue_health{venue="edgex"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="edgex"}[24h])
+ series: perp_venue_active_markets{venue="edgex"}
+
+ - slug: extended
+ name: Extended
+ tag: StarkEx-based perps venue
+ formula: "Count of active perp markets from Extended's public markets API on StarkEx."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="extended"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="extended"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="extended"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="extended"}[24h])
+ success: avg_over_time(perp_venue_health{venue="extended"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="extended"}[24h])
+ series: perp_venue_active_markets{venue="extended"}
+
+ - slug: aevo
+ name: Aevo
+ tag: Optimism L2 orderbook perps, options-focused history
+ formula: "Count of active perp instruments from Aevo's public markets endpoint on OP Stack."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="aevo"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="aevo"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="aevo"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="aevo"}[24h])
+ success: avg_over_time(perp_venue_health{venue="aevo"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="aevo"}[24h])
+ series: perp_venue_active_markets{venue="aevo"}
+
+ - slug: lighter
+ name: Lighter
+ tag: zk-rollup, zero taker fee
+ formula: "Count of markets with market_type==perp and status==active from Lighter's orderBookDetails. Native API, mainnet only."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="lighter"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="lighter"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="lighter"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="lighter"}[24h])
+ success: avg_over_time(perp_venue_health{venue="lighter"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="lighter"}[24h])
+ series: perp_venue_active_markets{venue="lighter"}
+
+ - slug: hyperliquid
+ name: Hyperliquid
+ tag: HyperBFT L1 perp DEX
+ formula: "Count of non-delisted assets from Hyperliquid info metaAndAssetCtxs. Active perp crypto markets only."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="hyperliquid"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="hyperliquid"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="hyperliquid"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="hyperliquid"}[24h])
+ success: avg_over_time(perp_venue_health{venue="hyperliquid"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="hyperliquid"}[24h])
+ series: perp_venue_active_markets{venue="hyperliquid"}
+
+ - slug: grvt
+ name: GRVT
+ tag: Hybrid CEX/DEX perps on ZK Stack
+ formula: "Count of active instruments from GRVT public markets endpoint on ZK Stack."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="grvt"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="grvt"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="grvt"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="grvt"}[24h])
+ success: avg_over_time(perp_venue_health{venue="grvt"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="grvt"}[24h])
+ series: perp_venue_active_markets{venue="grvt"}
+
+ - slug: dydx
+ name: dYdX v4
+ tag: Cosmos appchain orderbook perps
+ formula: "Count of non-deactivated markets from the dYdX v4 indexer perpetualMarkets endpoint."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="dydx"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="dydx"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="dydx"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="dydx"}[24h])
+ success: avg_over_time(perp_venue_health{venue="dydx"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="dydx"}[24h])
+ series: perp_venue_active_markets{venue="dydx"}
+
+ - slug: ostium
+ name: Ostium
+ tag: Synthetic perps on Arbitrum, FX and commodities alongside crypto
+ formula: "Count of active pairs from the Ostium subgraph on Arbitrum. Covers FX, commodities and crypto."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="ostium"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="ostium"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="ostium"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="ostium"}[24h])
+ success: avg_over_time(perp_venue_health{venue="ostium"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="ostium"}[24h])
+ series: perp_venue_active_markets{venue="ostium"}
+
+ - slug: pacifica
+ name: Pacifica
+ tag: Solana-native onchain perps
+ formula: "Count of active perp markets from onchain reads on the Pacifica Solana program state."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="pacifica"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="pacifica"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="pacifica"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="pacifica"}[24h])
+ success: avg_over_time(perp_venue_health{venue="pacifica"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="pacifica"}[24h])
+ series: perp_venue_active_markets{venue="pacifica"}
+
+ - slug: paradex
+ name: Paradex
+ tag: Starknet L2 perps
+ formula: "Count of active perp markets from the Paradex markets summary endpoint on Starknet L2."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="paradex"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="paradex"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="paradex"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="paradex"}[24h])
+ success: avg_over_time(perp_venue_health{venue="paradex"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="paradex"}[24h])
+ series: perp_venue_active_markets{venue="paradex"}
+
+ - slug: vertex
+ name: Vertex
+ tag: Arbitrum orderbook plus AMM hybrid
+ formula: "Count of active perp products from the Vertex public indexer on Arbitrum."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="vertex"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="vertex"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="vertex"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="vertex"}[24h])
+ success: avg_over_time(perp_venue_health{venue="vertex"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="vertex"}[24h])
+ series: perp_venue_active_markets{venue="vertex"}
+
+ - slug: polymarket
+ name: Polymarket
+ tag: Polygon perps, launched 2026-07
+ formula: "Count of listed instruments from the Polymarket public perps tickers endpoint on Polygon."
+ queries:
+ p50: avg_over_time(perp_venue_active_markets{venue="polymarket"}[24h])
+ p90: quantile_over_time(0.90, perp_venue_active_markets{venue="polymarket"}[24h])
+ p99: quantile_over_time(0.99, perp_venue_active_markets{venue="polymarket"}[24h])
+ mean: avg_over_time(perp_venue_active_markets{venue="polymarket"}[24h])
+ success: avg_over_time(perp_venue_health{venue="polymarket"}[24h])
+ sample_size: count_over_time(perp_venue_active_markets{venue="polymarket"}[24h])
+ series: perp_venue_active_markets{venue="polymarket"}
diff --git a/benchmarks/perp-cost-slope.yml b/benchmarks/perp-cost-slope.yml
new file mode 100644
index 00000000..ee8f3802
--- /dev/null
+++ b/benchmarks/perp-cost-slope.yml
@@ -0,0 +1,236 @@
+# OpenChainBench. Bench 117
+
+slug: perp-cost-slope
+number: "117"
+title: "Perp DEX cost invariance: all-in bps at $1M vs $1k, live slope ranked"
+seo_title: "Perp DEX cost at $1M vs $1k 2026"
+seo_description: "{{best_name}} leads perp cost invariance at {{best_p50}} cost slope (24h avg). Ratio of all-in bps at $1M vs $1k: oracle-priced venues score 1.00, orderbook venues above. Gains, GMX, Hyperliquid, dYdX ranked live."
+subtitle: "Ratio of all-in opening cost at $1,000,000 vs $1,000 notional, per venue per asset. A venue with flat cost at every size scores 1.00. A deeper book scores closer to 1.00 than a thin one. Oracle-priced venues (no orderbook to walk) always score 1.00 by construction."
+category: Trading
+status: live
+metric: "Cost slope ($1M / $1k)"
+unit: x
+higher_is_better: false
+
+seo_intro: |
+ Every perp DEX comparison article ranks venues by their published taker
+ fee rate. That number is size-independent by design: 4.5 bps on
+ Hyperliquid costs the same at $1,000 and $1,000,000 in taker fee alone.
+ What changes is the price impact from walking the orderbook. A $1M
+ market order eats deeper into the book than a $1k order, so the spread
+ component grows. Venues with no orderbook (oracle-priced: gains.trade,
+ GMX v2) are immune to this effect because there is no book to walk: the
+ protocol charges a flat percentage of notional regardless of size. On
+ those venues, a $1M trade costs exactly the same number of bps as a $1k
+ trade, giving a cost slope of 1.00. On an orderbook venue the slope
+ exceeds 1.00 by however much impact the extra notional adds. The wider
+ the gap above 1.00, the more the venue penalises large orders relative
+ to small ones. A venue whose visible book cannot absorb $1M at all shows
+ no slope because the $1M tier is skipped: that absence is a depth signal
+ stronger than any ratio. This benchmark derives its slope directly from
+ the existing perp-fees harness, which already measures all-in bps at
+ $1k, $10k, $100k and $1M per venue per asset every five minutes.
+
+abstract: |
+ The cost slope is the ratio of a venue's all-in opening cost at $1M
+ notional to its all-in cost at $1k notional. Both terms come from
+ perp_fees_all_in_bps_tier, the multi-tier companion series already
+ published by the perp-fees harness (bench 007). No new data collection
+ is required: the bench derives the ratio in PromQL at query time. A
+ ratio of 1.00 means the venue charges the same basis points per unit of
+ notional at $1M as at $1k, which is only possible for oracle-priced
+ venues where the fee is a flat percentage of position size with no
+ orderbook to walk. For orderbook venues the ratio rises above 1.00
+ because deeper book levels carry progressively wider prices. The
+ companion fill-rate panel shows the fraction of measurement cycles in
+ which the $1M tier was successfully filled: venues that cannot reliably
+ absorb $1M show a fill rate below 1.00, which is a stronger signal than
+ any slope number. Data is refreshed every five minutes from the
+ perp-fees harness. The chain tabs scope the board to ETH, BTC or SOL
+ individually so the slope of a thin SOL book is not mixed with a deep
+ ETH book.
+
+methodology:
+ - "Cadence: derived in PromQL from perp_fees_all_in_bps_tier, refreshed every 5 minutes by the perp-fees harness (bench 007)."
+ - "Cost slope = avg_over_time(all_in_bps_tier{notional=1000000}[24h]) / ignoring(notional) avg_over_time(all_in_bps_tier{notional=1000}[24h]) per venue per asset. A ratio of 1.00 means flat cost across sizes."
+ - "Oracle-priced venues (gains.trade, GMX v2): the fee is a flat percentage of position size with no orderbook. The $1M all-in equals the $1k all-in in bps exactly, so slope = 1.00 always."
+ - "Orderbook venues (Hyperliquid, dYdX, Lighter, Extended, Aster, edgeX): the $1M tier walks deeper into the book than the $1k tier, so the spread component grows and slope > 1.00."
+ - "Missing $1M tier: when the visible book cannot absorb $1M the harness skips the tier and increments perp_fees_tier_skipped_total rather than extrapolating. The slope PromQL returns no data for that venue, shown as N/A in the leaderboard."
+ - "Fill-rate panel: count_over_time(tier{notional=1000000}[24h]) / ignoring(notional) count_over_time(tier{notional=1000}[24h]) per venue per asset. 1.00 = tier always filled; 0.50 = tier filled half the time; 0.00 = tier never filled."
+ - "Chain tabs: the board is scoped per asset (ETH, BTC, SOL) because book depth differs significantly across assets and cross-asset averaging obscures the signal."
+ - "Failures and stale data: inherited from the perp-fees harness. A venue that errors keeps its last gauge and its health drops to 0."
+
+per_chain_explainer:
+ - slug: ETH
+ h2: "ETH perp cost slope: size impact on the deepest pair"
+ body: |
+ ETH-PERP is the deepest pair on every venue in the cohort. On orderbook venues the $1M
+ tier is usually filled because ETH books carry several million USD of visible depth on the
+ ask side. The slope is therefore computable for most venues on ETH, and the spread between
+ the oracle-priced 1.00 and the orderbook venues reveals the raw cost of depth-limited execution.
+ Gains and GMX always read 1.00 because their protocol charges a flat open fee regardless of
+ notional; there is no book to walk for ETH on those venues.
+ - slug: BTC
+ h2: "BTC perp cost slope: tightest books, flattest slopes"
+ body: |
+ BTC carries the highest open interest on most venues and typically has the tightest spread
+ at any given notional. Orderbook venues therefore show their flattest slopes on BTC: the
+ $1M impact is minimised by depth. Oracle-priced venues (gains, gmx) remain at exactly 1.00
+ regardless.
+ - slug: SOL
+ h2: "SOL perp cost slope: where thin books expose the gap"
+ body: |
+ SOL books are thinner than ETH or BTC on most venues. The $1M tier is frequently skipped
+ on smaller venues (shown as N/A), and on the venues that do fill it the slope is noticeably
+ higher than on ETH. This is the asset where the flat-cost advantage of oracle-priced venues
+ is most visible: gains and gmx score 1.00 while orderbook venues may show slopes of 1.50
+ or higher.
+
+findings:
+ - "{{best_name}} leads the cost-slope leaderboard at {{best_p50}} (24h average). A slope of 1.00 means the venue charges the same basis points per unit of notional at $1M as at $1k."
+ - "{{name:gains}} and {{name:gmx}} are the only venues in the cohort that structurally score 1.00: they use oracle pricing with no orderbook, so the fee is a flat percentage of position size regardless of trade size."
+ - "{{name:hyperliquid}} sits at {{p50:hyperliquid}} cost slope (24h avg). The deepest onchain orderbook in the cohort keeps its impact low, but it still exceeds 1.00 at $1M notional on most assets."
+ - "Venues showing N/A in the slope column cannot reliably fill a $1M market order. The fill-rate panel shows what fraction of measurement cycles the $1M tier was successfully absorbed."
+ - "The cost slope is asset-specific: use the chain tabs to compare ETH (deepest books), BTC (tightest books) and SOL (thinnest books) separately."
+
+source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/perp-fees
+
+prometheus:
+ window: 24h
+ expected_freshness_seconds: 1800
+ freshness_metric: perp_fees_all_in_bps_tier
+
+faq:
+ - q: "What does cost slope of 1.00 mean?"
+ a: "A cost slope of 1.00 means the all-in basis points for a $1M trade are identical to a $1k trade on that venue. This is only possible for oracle-priced venues where the fee is a flat percentage of notional and there is no orderbook impact term."
+ - q: "Why are gains.trade and GMX always 1.00?"
+ a: "Both venues use oracle pricing with no orderbook. Their open fee (totalPositionSizeFeeP on Gains, positionFeeFactor on GMX) is a flat percentage of position size. Trading $1M costs the same bps as $1k by construction. This is the core design tradeoff: flat cost at any size, in exchange for using a price oracle rather than a live orderbook."
+ - q: "What does N/A in the slope column mean?"
+ a: "The venue's visible book could not absorb $1M of notional during the measurement period. The perp-fees harness skips a tier rather than extrapolate when the book depth is insufficient, so the $1M all-in gauge is absent. No slope can be computed. The fill-rate panel shows what fraction of cycles the $1M tier was successfully filled."
+ - q: "Is a 1.00 slope always better?"
+ a: "For a trader placing a $1M order: yes, a flat cost is better than a rising one. For a trader placing a $1k order: the slope is irrelevant because both the taker fee and the spread at small notional dominate regardless. The headline perp-fees bench (bench 007) covers the $1k case; this bench covers the size-scaling dimension."
+ - q: "Can an orderbook venue ever match 1.00?"
+ a: "In theory an infinitely deep book would have zero price impact at any size, giving a slope of exactly 1.00. In practice no onchain orderbook reaches that depth for sizes above $100k on SOL or above $1M on ETH or BTC. The fill rate panel shows how often a venue actually absorbs the full notional."
+ - q: "How does this differ from the perp-fees bench?"
+ a: "perp-fees (bench 007) ranks venues by absolute all-in cost at $1k notional. This bench ranks the same venues by how much their cost grows between $1k and $1M. A venue cheap at $1k can be expensive at $1M if its book is thin. The two benches together give a full picture: absolute cost at small size plus sensitivity to larger size."
+
+dimensions:
+ chain:
+ - { value: ETH, label: ETH }
+ - { value: BTC, label: BTC }
+ - { value: SOL, label: SOL }
+
+panel_main_label: "Cost slope ($1M / $1k)"
+metric_panels:
+ - id: fill_rate_1m
+ label: "$1M fill rate"
+ metric: "avg(count_over_time(perp_fees_all_in_bps_tier{notional=\"1000000\"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{notional=\"1000\"}[24h])) * 100"
+ label_key: venue
+ unit: pct
+ higher_is_better: true
+ description: "Fraction of 5-minute cycles in which the venue's visible book successfully absorbed $1M of buy notional for the selected asset. 100% = always filled; oracle-priced venues always fill. A low number means the book is too thin to reliably absorb large orders."
+
+providers:
+ - slug: gains
+ name: gains.trade
+ tag: "Synthetic perps on Base, oracle-priced, flat cost at any size"
+ formula: "all_in_bps_tier{notional=1000000}[24h] / all_in_bps_tier{notional=1000}[24h]. Oracle-priced (flat % of notional, no orderbook): ratio always 1.00 by construction."
+ queries:
+ p50: avg_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000"}[24h])
+ p90: avg_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="100000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000"}[24h])
+ p99: avg_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="10000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000"}[24h])
+ mean: avg_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000"}[24h])
+ success: avg_over_time(perp_fees_health{venue="gains"}[24h])
+ sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000000"}[24h])
+ series: perp_fees_all_in_bps_tier{venue="gains",notional="1000000"}
+
+ - slug: gmx
+ name: GMX v2
+ tag: "Synthetics on Arbitrum, oracle-priced, flat cost at any size"
+ formula: "all_in_bps_tier{notional=1000000}[24h] / all_in_bps_tier{notional=1000}[24h]. Oracle-priced (positionFeeFactor flat % of notional, no orderbook): ratio always 1.00."
+ queries:
+ p50: avg_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000"}[24h])
+ p90: avg_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="100000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000"}[24h])
+ p99: avg_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="10000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000"}[24h])
+ mean: avg_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000"}[24h])
+ success: avg_over_time(perp_fees_health{venue="gmx"}[24h])
+ sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000000"}[24h])
+ series: perp_fees_all_in_bps_tier{venue="gmx",notional="1000000"}
+
+ - slug: hyperliquid
+ name: Hyperliquid
+ tag: "HyperBFT L1 perp DEX, deepest onchain book"
+ formula: "all_in_bps_tier{notional=1000000}[24h] / all_in_bps_tier{notional=1000}[24h]. Orderbook: $1M tier walks deeper than $1k, raising the spread component above 1.00."
+ queries:
+ p50: avg_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000"}[24h])
+ p90: avg_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="100000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000"}[24h])
+ p99: avg_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="10000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000"}[24h])
+ mean: avg_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000"}[24h])
+ success: avg_over_time(perp_fees_health{venue="hyperliquid"}[24h])
+ sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000000"}[24h])
+ series: perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000000"}
+
+ - slug: lighter
+ name: Lighter
+ tag: "zk-rollup, zero taker fee, onchain orderbook"
+ formula: "all_in_bps_tier{notional=1000000}[24h] / all_in_bps_tier{notional=1000}[24h]. Orderbook: $1M tier walks 100 levels of the Lighter zk-rollup book."
+ queries:
+ p50: avg_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000"}[24h])
+ p90: avg_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="100000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000"}[24h])
+ p99: avg_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="10000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000"}[24h])
+ mean: avg_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000"}[24h])
+ success: avg_over_time(perp_fees_health{venue="lighter"}[24h])
+ sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000000"}[24h])
+ series: perp_fees_all_in_bps_tier{venue="lighter",notional="1000000"}
+
+ - slug: dydx
+ name: dYdX v4
+ tag: "Cosmos appchain orderbook"
+ formula: "all_in_bps_tier{notional=1000000}[24h] / all_in_bps_tier{notional=1000}[24h]. Orderbook: full dYdX v4 indexer book walked at each tier."
+ queries:
+ p50: avg_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000"}[24h])
+ p90: avg_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="100000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000"}[24h])
+ p99: avg_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="10000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000"}[24h])
+ mean: avg_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000"}[24h])
+ success: avg_over_time(perp_fees_health{venue="dydx"}[24h])
+ sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000000"}[24h])
+ series: perp_fees_all_in_bps_tier{venue="dydx",notional="1000000"}
+
+ - slug: extended
+ name: Extended
+ tag: "Starknet perps, full public book"
+ formula: "all_in_bps_tier{notional=1000000}[24h] / all_in_bps_tier{notional=1000}[24h]. Orderbook: full public Starknet book walked at each tier."
+ queries:
+ p50: avg_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000"}[24h])
+ p90: avg_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="100000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000"}[24h])
+ p99: avg_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="10000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000"}[24h])
+ mean: avg_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000"}[24h])
+ success: avg_over_time(perp_fees_health{venue="extended"}[24h])
+ sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000000"}[24h])
+ series: perp_fees_all_in_bps_tier{venue="extended",notional="1000000"}
+
+ - slug: aster
+ name: Aster
+ tag: "BNB Chain perp DEX, Binance-style REST book"
+ formula: "all_in_bps_tier{notional=1000000}[24h] / all_in_bps_tier{notional=1000}[24h]. Orderbook: BNB Chain CLOB, fapi depth endpoint walked per tier."
+ queries:
+ p50: avg_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000"}[24h])
+ p90: avg_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="100000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000"}[24h])
+ p99: avg_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="10000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000"}[24h])
+ mean: avg_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000"}[24h])
+ success: avg_over_time(perp_fees_health{venue="aster"}[24h])
+ sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000000"}[24h])
+ series: perp_fees_all_in_bps_tier{venue="aster",notional="1000000"}
+
+ - slug: edgex
+ name: edgeX
+ tag: "Offchain CLOB perp DEX"
+ formula: "all_in_bps_tier{notional=1000000}[24h] / all_in_bps_tier{notional=1000}[24h]. Orderbook: offchain CLOB, public getDepth endpoint walked per tier."
+ queries:
+ p50: avg_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000"}[24h])
+ p90: avg_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="100000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000"}[24h])
+ p99: avg_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="10000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000"}[24h])
+ mean: avg_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000000"}[24h]) / ignoring(notional) avg_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000"}[24h])
+ success: avg_over_time(perp_fees_health{venue="edgex"}[24h])
+ sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000000"}[24h])
+ series: perp_fees_all_in_bps_tier{venue="edgex",notional="1000000"}
diff --git a/benchmarks/perp-mark-price-lag.yml b/benchmarks/perp-mark-price-lag.yml
new file mode 100644
index 00000000..63b848cb
--- /dev/null
+++ b/benchmarks/perp-mark-price-lag.yml
@@ -0,0 +1,180 @@
+# OpenChainBench. Bench 118
+
+slug: perp-mark-price-lag
+number: "118"
+title: "Perp DEX mark price fidelity: deviation from CEX spot reference, live"
+seo_title: "Perp DEX mark price accuracy 2026"
+seo_description: "{{best_name}} leads mark price fidelity at {{best_p50}} bps deviation from CEX spot (24h p50). Oracle-priced venues vs orderbook venues on ETH, BTC and SOL. gains.trade, GMX, Hyperliquid, dYdX, Lighter, Paradex ranked live."
+subtitle: "Median absolute deviation in basis points between each venue's mark price and a Binance spot reference, measured every 60 seconds on ETH, BTC and SOL. Lower means the venue's mark price stays closer to the CEX spot consensus. Oracle-priced venues use a real-time price feed anchored to CEX spot; orderbook venues derive their mark from their own book."
+category: Trading
+status: live
+metric: Mark price deviation
+unit: bps
+higher_is_better: false
+
+seo_intro: |
+ Every perpetual DEX publishes a mark price: the price used to compute
+ unrealised P&L, trigger liquidations and calculate funding. For a
+ trader, the mark price being far from the real market price is not a
+ minor inconvenience, it is a liquidation risk. A venue whose mark price
+ persistently sits 50 bps above the real price will liquidate a short at
+ a price that never existed on any exchange. This benchmark measures
+ how far each venue's live mark price sits from a Binance spot reference
+ on ETH, BTC and SOL, sampled every 60 seconds. Oracle-priced venues
+ (gains.trade, GMX v2) source their mark from a real-time price oracle
+ (Pyth or Chainlink) that itself tracks CEX spot continuously. Their
+ deviation from Binance spot is therefore structurally small: the oracle
+ interpolates from the same underlying CEX feed we use as reference.
+ Orderbook venues (Hyperliquid, dYdX, Lighter, Paradex) derive their
+ mark price from their own book, which can temporarily diverge from the
+ broader market during low-liquidity windows or fast-moving events. The
+ benchmark does not attempt to capture sub-second events such as
+ liquidation-triggering wicks, which require websocket resolution to
+ observe. It measures the persistent component: how far a venue's mark
+ price sits from CEX spot on a 60-second polling cadence.
+
+abstract: |
+ The harness polls each venue's published mark price every 60 seconds
+ alongside a Binance spot reference price (best bid/ask mid). Mark price
+ deviation is defined as the absolute value of (mark - reference) /
+ reference x 10000, in basis points. For oracle-priced venues
+ (gains.trade, GMX v2) the harness reads the Pyth price feed for the
+ same asset, which is the primary oracle both protocols weight heavily
+ in their mark price computation. Pyth updates sub-second from market
+ maker quotes that track CEX spot, so the deviation from Binance spot
+ is near zero structurally. For Hyperliquid the harness reads markPx
+ from the metaAndAssetCtxs endpoint. For dYdX v4 it reads indexPrice
+ from the perpetualMarkets indexer endpoint. For Lighter and Paradex
+ it reads the orderbook mid as an approximation of their mark price.
+ The headline metric is the 24h p50 of the deviation series, which
+ captures typical behaviour rather than isolated spikes. The p99 series
+ is exposed for those interested in tail risk. The signed deviation
+ (positive means the venue mark is above the reference) is published
+ separately as perp_mark_deviation_signed_bps.
+
+methodology:
+ - "Cadence: every 60 seconds in parallel across all venues. Reference price and venue mark prices are fetched in the same cycle, so the comparison is time-aligned."
+ - "Reference price: Binance REST /api/v3/ticker/bookTicker for the selected asset (ETHUSDT, BTCUSDT, SOLUSDT). The harness uses the mid of best bid and best ask as the reference."
+ - "gains.trade (Gains v8, oracle-priced): Pyth hermes API /v2/updates/price/latest for the ETH/USD, BTC/USD, SOL/USD feed IDs. Pyth is the primary real-time oracle Gains v8 weights in its mark price computation. Deviation from Binance spot is structurally near zero."
+ - "GMX v2 (oracle-priced): same Pyth hermes API endpoint. GMX v2 uses Chainlink as primary oracle; Pyth prices are an accepted proxy since both track CEX spot within 1 bps in normal conditions. Conservative labelling: formula tooltip notes the Pyth proxy."
+ - "Hyperliquid: POST /info {type:metaAndAssetCtxs} returns markPx per asset. This is the venue's own mark price used for P&L and liquidations, derived from their HyperBFT oracle."
+ - "dYdX v4: GET /v4/perpetualMarkets from the dYdX indexer returns indexPrice per market. indexPrice is the oracle-anchored price dYdX uses for margin and liquidation calculations."
+ - "Lighter: orderbook mid computed from GET /api/v1/orderBookOrders best bid and ask. Lighter does not publish a separate mark price endpoint; the orderbook mid is the closest available approximation."
+ - "Paradex: GET /markets endpoint returns markPrice per instrument on Starknet L2."
+ - "Deviation formula: abs(mark - reference) / reference x 10000, in basis points. Signed version (mark - reference) / reference x 10000 also published."
+ - "Failure handling: a venue that errors or times out leaves the previous gauge in place and increments perp_mark_fetch_errors_total. The health gauge drops to 0, flagging the row as stale in the UI."
+ - "Scope: this bench measures the persistent deviation visible at 60-second polling. Sub-second wick events that resolve within a single poll interval are not captured. The methodology page names this explicitly to avoid overstating what is measured."
+
+findings:
+ - "{{best_name}} leads mark price fidelity at {{best_p50}} bps median deviation from Binance spot (24h p50 across ETH, BTC and SOL)."
+ - "{{name:gains}} and {{name:gmx}} are the oracle-priced venues: their mark price is derived from Pyth, which itself aggregates from CEX market makers and tracks Binance spot within 1 bps in normal conditions. Their structural deviation is near zero."
+ - "{{name:hyperliquid}} sits at {{p50:hyperliquid}} bps median deviation. Hyperliquid's oracle is a volume-weighted median of external CEX prices, so it tracks spot closely but with a short latency window."
+ - "{{name:dydx}} sits at {{p50:dydx}} bps median deviation. dYdX uses an oracle-anchored index price that lags spot by at most a few seconds under normal conditions."
+ - "{{name:lighter}} and {{name:paradex}} derive their mark from their orderbook, which can diverge from spot during low-liquidity windows."
+ - "The p99 series (available in the Series tab) captures tail deviations. A high p99 relative to p50 signals occasional windows of elevated divergence."
+
+source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/perp-mark-price-lag
+
+prometheus:
+ window: 24h
+ expected_freshness_seconds: 600
+ freshness_metric: perp_mark_deviation_bps
+
+faq:
+ - q: "What is the mark price and why does its deviation matter?"
+ a: "The mark price is the price each venue uses to compute unrealised P&L, trigger liquidations and settle funding payments. A mark price that diverges significantly from the real market price creates liquidation risk: a long position can be margin-called at a loss even if the actual market price never touched the liquidation level."
+ - q: "Why do oracle-priced venues structurally win this bench?"
+ a: "Oracle-priced venues (gains.trade, GMX v2) derive their mark from an external price oracle such as Pyth or Chainlink. Both oracles aggregate from CEX market maker quotes that continuously track Binance spot, so the deviation is structurally near zero. Orderbook venues derive their mark from their own book, which can temporarily lag or diverge from the broader market."
+ - q: "What is the Pyth proxy used for gains.trade and GMX?"
+ a: "The harness reads the Pyth hermes REST API for the ETH/USD, BTC/USD and SOL/USD price IDs. This is used as the oracle-priced mark proxy for both gains and gmx. Gains v8 weights Pyth heavily; GMX v2 primarily uses Chainlink. The methodology tooltip on each row discloses this proxy. Chainlink ETH/USD on Arbitrum tracks Pyth ETH/USD within 1 bps in normal conditions."
+ - q: "Does this bench capture scam wicks?"
+ a: "No. Scam wicks that resolve within a single 60-second poll interval are not captured. This bench measures the persistent component of mark price divergence: how far a venue typically sits from CEX spot across a full day of 60-second samples. Sub-second wick analysis requires websocket resolution on both sides simultaneously."
+ - q: "What does the signed deviation metric show?"
+ a: "The signed deviation (positive = venue mark above reference, negative = below) is published as perp_mark_deviation_signed_bps. A persistent positive bias means the venue's mark is above CEX spot, which inflates longs' unrealised P&L while squeezing shorts. A persistent negative bias does the opposite."
+ - q: "Why is Lighter's mark price approximated from the orderbook mid?"
+ a: "Lighter does not expose a dedicated mark price endpoint in its public API. The orderbook best-bid-best-ask mid is the closest available approximation and is used consistently across all cycles."
+
+dimensions:
+ chain:
+ - { value: ETH, label: ETH }
+ - { value: BTC, label: BTC }
+ - { value: SOL, label: SOL }
+
+providers:
+ - slug: gains
+ name: gains.trade
+ tag: "Oracle-priced via Pyth on Base, structurally near-zero deviation"
+ formula: "24h p50 of abs(Pyth ETH/USD price - Binance spot mid) / Binance spot mid x 10000, sampled every 60 seconds. Pyth is the primary real-time oracle weighted by Gains v8. Binance spot mid is the reference."
+ queries:
+ p50: quantile_over_time(0.50, perp_mark_deviation_bps{venue="gains"}[24h])
+ p90: quantile_over_time(0.90, perp_mark_deviation_bps{venue="gains"}[24h])
+ p99: quantile_over_time(0.99, perp_mark_deviation_bps{venue="gains"}[24h])
+ mean: avg_over_time(perp_mark_deviation_bps{venue="gains"}[24h])
+ success: avg_over_time(perp_mark_health{venue="gains"}[24h])
+ sample_size: count_over_time(perp_mark_deviation_bps{venue="gains"}[24h])
+ series: perp_mark_deviation_bps{venue="gains"}
+
+ - slug: gmx
+ name: GMX v2
+ tag: "Oracle-priced, Pyth proxy for Chainlink on Arbitrum"
+ formula: "24h p50 of abs(Pyth ETH/USD price - Binance spot mid) / Binance spot mid x 10000, sampled every 60 seconds. GMX v2 uses Chainlink as primary oracle; Pyth is used as an accepted proxy. Disclosed in methodology."
+ queries:
+ p50: quantile_over_time(0.50, perp_mark_deviation_bps{venue="gmx"}[24h])
+ p90: quantile_over_time(0.90, perp_mark_deviation_bps{venue="gmx"}[24h])
+ p99: quantile_over_time(0.99, perp_mark_deviation_bps{venue="gmx"}[24h])
+ mean: avg_over_time(perp_mark_deviation_bps{venue="gmx"}[24h])
+ success: avg_over_time(perp_mark_health{venue="gmx"}[24h])
+ sample_size: count_over_time(perp_mark_deviation_bps{venue="gmx"}[24h])
+ series: perp_mark_deviation_bps{venue="gmx"}
+
+ - slug: hyperliquid
+ name: Hyperliquid
+ tag: "HyperBFT L1, markPx from metaAndAssetCtxs"
+ formula: "24h p50 of abs(Hyperliquid markPx - Binance spot mid) / Binance spot mid x 10000, sampled every 60 seconds. markPx from POST /info {type:metaAndAssetCtxs}."
+ queries:
+ p50: quantile_over_time(0.50, perp_mark_deviation_bps{venue="hyperliquid"}[24h])
+ p90: quantile_over_time(0.90, perp_mark_deviation_bps{venue="hyperliquid"}[24h])
+ p99: quantile_over_time(0.99, perp_mark_deviation_bps{venue="hyperliquid"}[24h])
+ mean: avg_over_time(perp_mark_deviation_bps{venue="hyperliquid"}[24h])
+ success: avg_over_time(perp_mark_health{venue="hyperliquid"}[24h])
+ sample_size: count_over_time(perp_mark_deviation_bps{venue="hyperliquid"}[24h])
+ series: perp_mark_deviation_bps{venue="hyperliquid"}
+
+ - slug: dydx
+ name: dYdX v4
+ tag: "Cosmos appchain, indexPrice from perpetualMarkets indexer"
+ formula: "24h p50 of abs(dYdX indexPrice - Binance spot mid) / Binance spot mid x 10000, sampled every 60 seconds. indexPrice from GET /v4/perpetualMarkets on the dYdX indexer."
+ queries:
+ p50: quantile_over_time(0.50, perp_mark_deviation_bps{venue="dydx"}[24h])
+ p90: quantile_over_time(0.90, perp_mark_deviation_bps{venue="dydx"}[24h])
+ p99: quantile_over_time(0.99, perp_mark_deviation_bps{venue="dydx"}[24h])
+ mean: avg_over_time(perp_mark_deviation_bps{venue="dydx"}[24h])
+ success: avg_over_time(perp_mark_health{venue="dydx"}[24h])
+ sample_size: count_over_time(perp_mark_deviation_bps{venue="dydx"}[24h])
+ series: perp_mark_deviation_bps{venue="dydx"}
+
+ - slug: lighter
+ name: Lighter
+ tag: "zk-rollup, orderbook mid used as mark price proxy"
+ formula: "24h p50 of abs(Lighter orderbook mid - Binance spot mid) / Binance spot mid x 10000, sampled every 60 seconds. Lighter does not expose a dedicated mark price endpoint; the orderbook mid is used as an approximation."
+ queries:
+ p50: quantile_over_time(0.50, perp_mark_deviation_bps{venue="lighter"}[24h])
+ p90: quantile_over_time(0.90, perp_mark_deviation_bps{venue="lighter"}[24h])
+ p99: quantile_over_time(0.99, perp_mark_deviation_bps{venue="lighter"}[24h])
+ mean: avg_over_time(perp_mark_deviation_bps{venue="lighter"}[24h])
+ success: avg_over_time(perp_mark_health{venue="lighter"}[24h])
+ sample_size: count_over_time(perp_mark_deviation_bps{venue="lighter"}[24h])
+ series: perp_mark_deviation_bps{venue="lighter"}
+
+ - slug: paradex
+ name: Paradex
+ tag: "Starknet appchain, markPrice from /markets"
+ formula: "24h p50 of abs(Paradex markPrice - Binance spot mid) / Binance spot mid x 10000, sampled every 60 seconds. markPrice from GET /markets on Starknet L2."
+ queries:
+ p50: quantile_over_time(0.50, perp_mark_deviation_bps{venue="paradex"}[24h])
+ p90: quantile_over_time(0.90, perp_mark_deviation_bps{venue="paradex"}[24h])
+ p99: quantile_over_time(0.99, perp_mark_deviation_bps{venue="paradex"}[24h])
+ mean: avg_over_time(perp_mark_deviation_bps{venue="paradex"}[24h])
+ success: avg_over_time(perp_mark_health{venue="paradex"}[24h])
+ sample_size: count_over_time(perp_mark_deviation_bps{venue="paradex"}[24h])
+ series: perp_mark_deviation_bps{venue="paradex"}
diff --git a/benchmarks/perp-open-interest.yml b/benchmarks/perp-open-interest.yml
deleted file mode 100644
index e93cb926..00000000
--- a/benchmarks/perp-open-interest.yml
+++ /dev/null
@@ -1,259 +0,0 @@
-# OpenChainBench. Bench № 042
-
-slug: perp-open-interest
-number: "042"
-title: Perp DEX open interest, live USD notional ranked
-seo_title: "Perp DEX open interest 2026"
-seo_description: "Live perpetual futures open interest in USD across 13 major perp DEX venues: Hyperliquid, Aster, Lighter, GRVT, Extended, Ostium, Pacifica, Vertex, dYdX, Aevo, Paradex, gains.trade, Polymarket. Refreshed every 5 minutes."
-subtitle: Live aggregate open interest in USD across 13 major decentralized perpetual futures venues. Higher means more notional sits open. Read live from each venue's public API.
-category: Trading
-status: live
-metric: Open interest
-unit: usd
-higher_is_better: true
-
-seo_intro: |
- This benchmark ranks the major onchain perp venues by aggregate open
- interest, in USD. Open interest is the canonical depth-of-market
- number for perpetual futures: it tells you how much notional sits
- open right now, on every market, on a given venue. We poll each
- venue's public API every 5 minutes, sum open interest across all
- listed perps, and publish one gauge per venue. The cohort covers 13
- venues: Hyperliquid, Aster, Lighter, GRVT, Extended, Ostium, Pacifica,
- Vertex, dYdX v4, Aevo, Paradex and gains.trade. Headline number is the 24h average
- of the live OI gauge so a single noisy print does not move the
- ranking.
-
-abstract: |
- The bench polls 13 perp DEX venues every 5 minutes for live open
- interest and exposes a USD-aggregated gauge per venue. Each venue's
- public API is queried directly (Hyperliquid info metaAndAssetCtxs,
- Aster fapi openInterest, Lighter orderBookDetails, GRVT public
- markets, Extended API, Ostium subgraph, Pacifica onchain reads,
- Vertex indexer, dYdX v4 indexer, Aevo public markets, Paradex
- markets summary, gains.trade trading-variables per collateral across
- Arbitrum/Base/Polygon/ApeChain) and the notional is summed in USD
- across all listed perps. Each value carries a freshness timestamp and a health
- gauge so a stale or errored venue does not poison the leaderboard.
- Higher is better.
-
-methodology:
- - "Convention: open interest counts each contract once (one side), the standard used by CME, Binance and Coinglass, and the only convention comparable across venues. Aggregators can differ: DefiLlama doubles Lighter and Ostium (long plus short), folds 1,682 options markets into Paradex, and adds HIP-3 sub-dex OI to Hyperliquid. Where our number is about half of theirs, that is the convention gap, not missing data; five venues where conventions align match DefiLlama within 2 percent."
- - "Cadence: every 5 minutes per venue in parallel, 10 second timeout per request."
- - "Hyperliquid: info metaAndAssetCtxs, openInterest summed across all assets and priced in USD using the venue's own mark price."
- - "Aster: fapi openInterest endpoint per instrument, summed across all listed USDT perps."
- - "Lighter: orderBookDetails per market, open interest summed across markets in USD."
- - "GRVT: public markets endpoint, open interest per instrument summed in USD."
- - "Extended: public markets API, open interest per instrument summed in USD."
- - "Ostium: subgraph read for open interest across all listed pairs on Arbitrum."
- - "Pacifica: onchain reads on the Solana program state for aggregate open interest."
- - "Vertex: public indexer, open interest per product summed across all perps."
- - "dYdX v4: indexer perpetualMarkets, openInterest per market summed in USD."
- - "Aevo: public markets endpoint, open interest per instrument summed in USD."
- - "Paradex: markets summary endpoint on Starknet L2, open interest summed across perps."
- - "gains.trade: trading-variables endpoint per chain backend (Arbitrum, Base, Polygon, ApeChain), per-collateral pairOis summed then multiplied by collateralPriceUsd, summed across all chains."
- - "Headline. avg_over_time of the live OI gauge over the last 24 hours, so a one-print spike does not move the ranking. The Series tab plots the raw gauge."
- - "Failures. A venue that errors or times out keeps its last gauge value and its perp_venue_health gauge drops toward 0; the leaderboard tags it as stale."
-
-findings:
- - "{{best_name}} currently leads the cohort at {{best_p50}} of open interest (24h average) across 13 measured perp DEX venues."
- - "{{name:hyperliquid}} sits at {{p50:hyperliquid}} of open interest. HyperBFT orderbook is the deepest decentralized perp venue by a wide margin."
- - "{{name:lighter}} clocks {{p50:lighter}} of open interest. Fully onchain orderbook on zkSync with zero taker fee."
- - "{{name:aster}} sits at {{p50:aster}} of open interest. BNB Chain perps DEX with a Binance-compatible API surface."
- - "{{name:grvt}} sits at {{p50:grvt}} of open interest. Hybrid CEX/DEX perps on ZK Stack."
- - "{{name:extended}} sits at {{p50:extended}} of open interest. StarkEx-based perps venue."
- - "{{name:pacifica}} sits at {{p50:pacifica}} of open interest. Solana-native onchain perps."
- - "{{name:dydx}} sits at {{p50:dydx}} of open interest. Cosmos appchain orderbook, one of the oldest decentralized perp venues in the cohort."
- - "{{name:ostium}} sits at {{p50:ostium}} of open interest. Synthetic perps on Arbitrum covering FX and commodities alongside crypto."
- - "{{name:vertex}} sits at {{p50:vertex}} of open interest. Arbitrum orderbook plus AMM hybrid."
- - "{{name:paradex}} sits at {{p50:paradex}} of open interest. Starknet L2 perps."
- - "{{name:aevo}} sits at {{p50:aevo}} of open interest. Optimism L2 orderbook perps, options-focused history."
- - "{{name:gains}} sits at {{p50:gains}} of open interest. Synthetic perps across Arbitrum, Base, Polygon and ApeChain."
-
-disclaimer: "gains.trade aggregates open interest across four chains (Arbitrum, Base, Polygon, ApeChain) into a single number, while most other venues operate on one chain. This inflates gains.trade's total relative to single-chain venues and makes a direct OI comparison unfair. The per-venue tags call this out; for a single-chain view of gains.trade, use the per-chain breakdown once available."
-
-source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/perp-cohort-stats
-
-prometheus:
- window: 24h
- expected_freshness_seconds: 1800
-
-faq:
- - q: "What does this benchmark measure?"
- a: "The live aggregate open interest of 13 major perp DEX venues in USD. Headline is the 24h average of the live OI gauge so single prints do not skew the ranking."
- - q: "How is open interest different from volume?"
- a: "Volume counts every trade that crosses the book during a window. Open interest counts notional that currently sits open, regardless of when it was opened. A high-volume venue with high turnover can carry lower OI than a slower venue with sticky positions."
- - q: "Which venues are in the cohort?"
- a: "Hyperliquid, Aster, Lighter, GRVT, Extended, Ostium, Pacifica, Vertex, dYdX v4, Aevo, Paradex, gains.trade and Polymarket, the 13 decentralized perp venues whose public APIs expose open interest cleanly enough to compare without backfilling. Other venues join as their endpoints document the same series."
- - q: "Is OI a good proxy for venue health?"
- a: "It is one of the cleanest. A venue can spike volume with wash trading or incentive programs, but real OI is harder to fake because it ties up margin. Compared against perp-volume-share, persistent OI dominance signals real position-holding flow rather than turnover."
-
-providers:
- - slug: hyperliquid
- name: Hyperliquid
- tag: HyperBFT L1 perp DEX
- formula: "Open interest from Hyperliquid info metaAndAssetCtxs, summed across all assets and priced in USD using the venue's mark price; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="hyperliquid"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="hyperliquid"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="hyperliquid"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="hyperliquid"}[24h])
- success: avg_over_time(perp_venue_health{venue="hyperliquid"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="hyperliquid"}[24h])
- series: perp_venue_oi_usd{venue="hyperliquid"}
-
- - slug: polymarket
- name: Polymarket
- tag: Polygon perps by Polymarket, launched 2026-07, public info API
- formula: "open_interest (base units) times mark_price from the Polymarket perps tickers endpoint, summed across all listed instruments in USD; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="polymarket"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="polymarket"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="polymarket"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="polymarket"}[24h])
- success: avg_over_time(perp_venue_health{venue="polymarket"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="polymarket"}[24h])
- series: perp_venue_oi_usd{venue="polymarket"}
-
- - slug: aster
- name: Aster
- tag: BNB Chain perps DEX, Binance-compatible API
- formula: "Open interest from Aster fapi openInterest per instrument, summed across all listed USDT perps; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="aster"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="aster"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="aster"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="aster"}[24h])
- success: avg_over_time(perp_venue_health{venue="aster"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="aster"}[24h])
- series: perp_venue_oi_usd{venue="aster"}
-
- - slug: lighter
- name: Lighter
- tag: zk-rollup, zero taker fee
- formula: "Open interest from Lighter orderBookDetails per market, summed across markets in USD; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="lighter"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="lighter"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="lighter"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="lighter"}[24h])
- success: avg_over_time(perp_venue_health{venue="lighter"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="lighter"}[24h])
- series: perp_venue_oi_usd{venue="lighter"}
-
- - slug: grvt
- name: GRVT
- tag: Hybrid CEX/DEX perps on ZK Stack
- formula: "Open interest from GRVT public markets endpoint per instrument, summed in USD; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="grvt"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="grvt"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="grvt"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="grvt"}[24h])
- success: avg_over_time(perp_venue_health{venue="grvt"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="grvt"}[24h])
- series: perp_venue_oi_usd{venue="grvt"}
-
- - slug: extended
- name: Extended
- tag: StarkEx-based perps venue
- formula: "Open interest from Extended public markets API per instrument, summed in USD; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="extended"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="extended"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="extended"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="extended"}[24h])
- success: avg_over_time(perp_venue_health{venue="extended"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="extended"}[24h])
- series: perp_venue_oi_usd{venue="extended"}
-
- - slug: ostium
- name: Ostium
- tag: Synthetic perps on Arbitrum, FX and commodities alongside crypto
- formula: "Open interest from the Ostium subgraph across all listed pairs on Arbitrum, summed in USD; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="ostium"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="ostium"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="ostium"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="ostium"}[24h])
- success: avg_over_time(perp_venue_health{venue="ostium"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="ostium"}[24h])
- series: perp_venue_oi_usd{venue="ostium"}
-
- - slug: pacifica
- name: Pacifica
- tag: Solana-native onchain perps
- formula: "Open interest read onchain from the Pacifica Solana program state, summed across all listed perps; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="pacifica"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="pacifica"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="pacifica"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="pacifica"}[24h])
- success: avg_over_time(perp_venue_health{venue="pacifica"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="pacifica"}[24h])
- series: perp_venue_oi_usd{venue="pacifica"}
-
- - slug: vertex
- name: Vertex
- tag: Arbitrum orderbook plus AMM hybrid
- formula: "Open interest from the Vertex public indexer per product, summed across all perps in USD; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="vertex"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="vertex"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="vertex"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="vertex"}[24h])
- success: avg_over_time(perp_venue_health{venue="vertex"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="vertex"}[24h])
- series: perp_venue_oi_usd{venue="vertex"}
-
- - slug: dydx
- name: dYdX v4
- tag: Cosmos appchain orderbook perps
- formula: "Open interest from the dYdX v4 indexer perpetualMarkets endpoint, summed per market in USD; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="dydx"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="dydx"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="dydx"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="dydx"}[24h])
- success: avg_over_time(perp_venue_health{venue="dydx"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="dydx"}[24h])
- series: perp_venue_oi_usd{venue="dydx"}
-
- - slug: aevo
- name: Aevo
- tag: Optimism L2 orderbook perps, options-focused history
- formula: "Open interest from the Aevo public markets endpoint per instrument, summed in USD; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="aevo"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="aevo"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="aevo"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="aevo"}[24h])
- success: avg_over_time(perp_venue_health{venue="aevo"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="aevo"}[24h])
- series: perp_venue_oi_usd{venue="aevo"}
-
- - slug: paradex
- name: Paradex
- tag: Starknet L2 perps
- formula: "Open interest from the Paradex markets summary endpoint on Starknet L2, summed across perps in USD; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="paradex"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="paradex"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="paradex"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="paradex"}[24h])
- success: avg_over_time(perp_venue_health{venue="paradex"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="paradex"}[24h])
- series: perp_venue_oi_usd{venue="paradex"}
-
- - slug: gains
- name: gains.trade
- tag: Synthetic perps across Arbitrum + Base + Polygon + ApeChain (4-chain aggregate — not directly comparable to single-chain venues)
- formula: "Open interest from the gains.trade trading-variables endpoint per chain backend: per-collateral pairOis long+short, normalized by precision x collateralPriceUsd, summed across 4 chains; headline is the 24h time average."
- queries:
- p50: avg_over_time(perp_venue_oi_usd{venue="gains"}[24h])
- p90: quantile_over_time(0.90, perp_venue_oi_usd{venue="gains"}[24h])
- p99: quantile_over_time(0.99, perp_venue_oi_usd{venue="gains"}[24h])
- mean: avg_over_time(perp_venue_oi_usd{venue="gains"}[24h])
- success: avg_over_time(perp_venue_health{venue="gains"}[24h])
- sample_size: count_over_time(perp_venue_oi_usd{venue="gains"}[24h])
- series: perp_venue_oi_usd{venue="gains"}
diff --git a/benchmarks/perp-protocol-longevity.yml b/benchmarks/perp-protocol-longevity.yml
new file mode 100644
index 00000000..efedea82
--- /dev/null
+++ b/benchmarks/perp-protocol-longevity.yml
@@ -0,0 +1,174 @@
+# OpenChainBench. Bench 119
+
+slug: perp-protocol-longevity
+number: "119"
+title: "Perp DEX security track record: days without a recorded exploit, live"
+seo_title: "Perp DEX security track record 2026"
+seo_description: "gains.trade leads perp DEX security longevity at {{p50:gains}} days without a recorded exploit. GMX, Hyperliquid, dYdX v4, Lighter, Paradex ranked by days since last incident on rekt.news and public post-mortems."
+subtitle: "Days each perp DEX has operated without a recorded security exploit, counted live from a versioned incident registry sourced from rekt.news and public post-mortems. Higher means a longer clean track record. Oracle-priced venues with immutable smart contracts have a structural advantage: there is no custodial hot wallet and no privileged admin key that can drain the vault."
+category: Trading
+status: live
+metric: Days since last exploit
+unit: count
+higher_is_better: true
+
+disclaimer: |
+ This benchmark counts recorded incidents only. Absence from the registry does not prove a protocol is exploit-free: it means no incident matching the criteria (direct theft or loss of user funds via a security vulnerability) has been documented in the tracked sources as of the registry timestamp shown on each row. The registry is human-curated and updated when a new incident is confirmed. The registry date and source links are embedded in the harness.
+
+seo_intro: |
+ Smart contract exploits are the tail risk that every DeFi protocol carries.
+ For a perp DEX the consequences are asymmetric: a single reentrancy or
+ oracle manipulation event can wipe all liquidity from the vault while
+ positions are still open. The DeFi ecosystem has a public, reasonably
+ comprehensive ledger of these events in rekt.news and community
+ post-mortems. This benchmark converts that ledger into a live counter:
+ how many days has each venue operated without a recorded security exploit
+ that resulted in direct theft or loss of user funds via a protocol
+ vulnerability. gains.trade has been live since December 2021, more than
+ four years, with no recorded exploit in the tracked sources as of the
+ registry date. GMX had an exploit in July 2025 estimated at around 42
+ million USD via a reentrancy vulnerability. Hyperliquid, dYdX v4,
+ Lighter and Paradex have no recorded protocol exploits. The harness
+ computes the counter live each day from a versioned YAML registry
+ committed to the harness repository. The registry is semi-automated:
+ a daily job polls rekt.news for new entries matching the venue slugs
+ and alerts a maintainer to confirm before committing. The counter
+ therefore reflects confirmed incidents, not unverified reports.
+
+abstract: |
+ The harness maintains a versioned incident registry (registry.yaml
+ embedded in the binary at build time). For each venue the registry
+ holds the protocol launch date and a list of confirmed security
+ incidents with date, estimated loss in USD, incident type and source
+ URL. The live counter is (current_timestamp - max(launch_date,
+ last_incident_date)) / 86400 in days. A venue with no incidents uses
+ the launch date as the start of the clean streak. The harness emits
+ three gauges per venue: perp_protocol_days_clean (the live counter),
+ perp_protocol_incidents_total (count of recorded incidents), and
+ perp_protocol_incident_amount_usd (cumulative USD lost in recorded
+ incidents). The registry includes only incidents that resulted in
+ direct theft or permanent loss of user funds via a protocol
+ vulnerability. Oracle price manipulation without smart contract
+ compromise, market-structure events (liquidation cascades,
+ governance disputes) and front-end phishing are excluded and noted
+ separately in the per-venue formula tooltip.
+
+methodology:
+ - "Registry: a versioned YAML file embedded in the harness binary at build time. Fields per incident: venue_slug, date (YYYY-MM-DD), amount_usd, type (exploit, oracle-manipulation, admin-key, bridge), source_url."
+ - "Counter formula: (Unix timestamp at query time - Unix timestamp of max(launch_date, last_incident_date)) / 86400. Emitted as perp_protocol_days_clean{venue}."
+ - "Incident criteria: direct theft or permanent loss of user funds via a documented smart contract vulnerability or privileged key compromise. Oracle manipulation without contract exploit is excluded. Market-structure events (liquidation cascades, large-position forced close) are excluded."
+ - "gains.trade: launched 2021-12-01. No incidents in tracked sources as of registry date 2026-08-02. Clean streak = days since 2021-12-01."
+ - "GMX v2: launched 2023-08-01. Incident 2025-07-09: reentrancy in vault and order-flow logic, estimated loss USD 42 000 000, source coinperps.xyz post-mortem. Clean streak = days since 2025-07-09."
+ - "Hyperliquid: launched 2023-11-01. No protocol exploit in tracked sources. Note: the March 2025 JellyJelly forced close was a market-structure event (large-position liquidation cascade triggering a governance emergency), not a smart contract exploit, and is excluded per criteria."
+ - "dYdX v4: launched on the Cosmos appchain 2023-10-01. No protocol exploit on v4. dYdX v1 and v2 on Ethereum had oracle manipulation events that are excluded here (different contract, different era)."
+ - "Lighter: launched 2023-07-01. No incidents in tracked sources."
+ - "Paradex: launched 2023-10-01. No incidents in tracked sources."
+ - "Registry update process: a nightly job polls rekt.news for new entries matching venue slugs and raises a review alert. A maintainer confirms and commits the updated registry.yaml. The harness auto-rebuilds and the counter updates on the next deploy cycle."
+ - "Survivorship note: older protocols have had more calendar time in which an exploit could occur. A long streak is a positive signal but is partly explained by age. The perp_protocol_incidents_total counter lets readers weight the streak against the full history."
+
+findings:
+ - "{{best_name}} leads the security longevity leaderboard at {{best_p50}} days without a recorded exploit (24h live counter)."
+ - "{{name:gains}} has operated since {{gains_launch}} with no recorded exploit in rekt.news or public post-mortems as of the registry date. The synthetic architecture (no custodial vault holding user funds outright, no privileged admin key) removes the most common attack surfaces."
+ - "{{name:gmx}} clocks {{p50:gmx}} days since its July 2025 reentrancy exploit (estimated USD 42 million). The incident involved a reentrancy in the vault and order-flow logic in GMX v2. The streak counter restarts from the incident date."
+ - "{{name:hyperliquid}}, {{name:dydx}}, {{name:lighter}} and {{name:paradex}} have no recorded protocol exploits. Their clean streaks start from their respective launch dates."
+ - "The perp_protocol_incident_amount_usd gauge shows cumulative confirmed losses per venue, allowing a risk-adjusted comparison alongside the day counter."
+
+source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/perp-protocol-longevity
+
+prometheus:
+ window: 24h
+ expected_freshness_seconds: 86400
+ freshness_metric: perp_protocol_days_clean
+
+faq:
+ - q: "What counts as a recordable incident?"
+ a: "Direct theft or permanent loss of user funds via a documented smart contract vulnerability or privileged key compromise. Oracle price manipulation events that do not involve a contract exploit are excluded. Market-structure events such as liquidation cascades triggered by large positions are excluded. The registry source URL for each entry is available in the harness YAML."
+ - q: "Why does gains.trade have such a long clean streak?"
+ a: "gains.trade launched in December 2021 and has not had a recorded exploit in the tracked sources as of the registry date, giving a streak of over four years. The synthetic architecture uses oracle pricing (no custodial AMM vault that can be drained) and immutable smart contracts without privileged admin keys on critical paths, which removes the most common attack surfaces."
+ - q: "Why is Hyperliquid's JellyJelly event not counted?"
+ a: "The March 2025 JellyJelly event was a market-structure incident: a large whale position was force-closed by the validator committee after a liquidation cascade threatened the insurance fund. No smart contract vulnerability was exploited and no user funds were stolen via a protocol bug. The methodology excludes market-structure events to keep the metric focused on smart contract security."
+ - q: "Does a long streak prove the protocol is safe?"
+ a: "No. A long clean streak is a positive signal and the best available public evidence of resilience under live conditions. It does not prove the code is exploit-free. Undiscovered vulnerabilities can exist for years before being found. The streak should be read alongside the protocol's audit history and architecture, not as a standalone guarantee."
+ - q: "How often is the registry updated?"
+ a: "A nightly job polls rekt.news and other public sources for entries matching the tracked venue slugs and raises an alert for manual review. A maintainer confirms and commits any new incidents, triggering a harness rebuild. The registry timestamp is visible in the harness source at registry.yaml."
+ - q: "Does this benchmark cover all historical incidents?"
+ a: "The benchmark covers incidents recorded in rekt.news and publicly disclosed post-mortems for the specific protocol deployments tracked. Incidents on predecessor versions (GMX v1, dYdX v1/v2), front-end phishing attacks and incidents on third-party integrations built on top of these protocols are out of scope."
+
+providers:
+ - slug: gains
+ name: gains.trade
+ tag: "Synthetic perps on Base, launched 2021-12-01, no recorded exploit"
+ formula: "Live counter: (current_unix - 2021-12-01T00:00:00Z) / 86400. No incidents in the registry as of 2026-08-02. Counter = days since launch."
+ queries:
+ p50: perp_protocol_days_clean{venue="gains"}
+ p90: perp_protocol_days_clean{venue="gains"}
+ p99: perp_protocol_days_clean{venue="gains"}
+ mean: perp_protocol_days_clean{venue="gains"}
+ success: perp_protocol_health{venue="gains"}
+ sample_size: count_over_time(perp_protocol_days_clean{venue="gains"}[24h])
+ series: perp_protocol_days_clean{venue="gains"}
+
+ - slug: lighter
+ name: Lighter
+ tag: "zk-rollup, launched 2023-07-01, no recorded exploit"
+ formula: "Live counter: (current_unix - 2023-07-01T00:00:00Z) / 86400. No incidents in the registry as of 2026-08-02. Counter = days since launch."
+ queries:
+ p50: perp_protocol_days_clean{venue="lighter"}
+ p90: perp_protocol_days_clean{venue="lighter"}
+ p99: perp_protocol_days_clean{venue="lighter"}
+ mean: perp_protocol_days_clean{venue="lighter"}
+ success: perp_protocol_health{venue="lighter"}
+ sample_size: count_over_time(perp_protocol_days_clean{venue="lighter"}[24h])
+ series: perp_protocol_days_clean{venue="lighter"}
+
+ - slug: dydx
+ name: dYdX v4
+ tag: "Cosmos appchain, launched 2023-10-01, no recorded exploit on v4"
+ formula: "Live counter: (current_unix - 2023-10-01T00:00:00Z) / 86400. No incidents recorded for the v4 Cosmos appchain deployment. Counter = days since v4 launch."
+ queries:
+ p50: perp_protocol_days_clean{venue="dydx"}
+ p90: perp_protocol_days_clean{venue="dydx"}
+ p99: perp_protocol_days_clean{venue="dydx"}
+ mean: perp_protocol_days_clean{venue="dydx"}
+ success: perp_protocol_health{venue="dydx"}
+ sample_size: count_over_time(perp_protocol_days_clean{venue="dydx"}[24h])
+ series: perp_protocol_days_clean{venue="dydx"}
+
+ - slug: hyperliquid
+ name: Hyperliquid
+ tag: "HyperBFT L1, launched 2023-11-01, no recorded smart contract exploit"
+ formula: "Live counter: (current_unix - 2023-11-01T00:00:00Z) / 86400. No protocol exploit recorded. Note: the March 2025 JellyJelly forced close was a market-structure event, not a smart contract exploit, and is excluded per methodology."
+ queries:
+ p50: perp_protocol_days_clean{venue="hyperliquid"}
+ p90: perp_protocol_days_clean{venue="hyperliquid"}
+ p99: perp_protocol_days_clean{venue="hyperliquid"}
+ mean: perp_protocol_days_clean{venue="hyperliquid"}
+ success: perp_protocol_health{venue="hyperliquid"}
+ sample_size: count_over_time(perp_protocol_days_clean{venue="hyperliquid"}[24h])
+ series: perp_protocol_days_clean{venue="hyperliquid"}
+
+ - slug: paradex
+ name: Paradex
+ tag: "Starknet appchain, launched 2023-10-01, no recorded exploit"
+ formula: "Live counter: (current_unix - 2023-10-01T00:00:00Z) / 86400. No incidents in the registry as of 2026-08-02. Counter = days since launch."
+ queries:
+ p50: perp_protocol_days_clean{venue="paradex"}
+ p90: perp_protocol_days_clean{venue="paradex"}
+ p99: perp_protocol_days_clean{venue="paradex"}
+ mean: perp_protocol_days_clean{venue="paradex"}
+ success: perp_protocol_health{venue="paradex"}
+ sample_size: count_over_time(perp_protocol_days_clean{venue="paradex"}[24h])
+ series: perp_protocol_days_clean{venue="paradex"}
+
+ - slug: gmx
+ name: GMX v2
+ tag: "Synthetics on Arbitrum, launched 2023-08-01, exploit 2025-07-09"
+ formula: "Live counter: (current_unix - 2025-07-09T00:00:00Z) / 86400. Incident: reentrancy in vault and order-flow logic, est. USD 42 000 000, source coinperps.xyz post-mortem 2025-07-09. Counter restarts from incident date."
+ queries:
+ p50: perp_protocol_days_clean{venue="gmx"}
+ p90: perp_protocol_days_clean{venue="gmx"}
+ p99: perp_protocol_days_clean{venue="gmx"}
+ mean: perp_protocol_days_clean{venue="gmx"}
+ success: perp_protocol_health{venue="gmx"}
+ sample_size: count_over_time(perp_protocol_days_clean{venue="gmx"}[24h])
+ series: perp_protocol_days_clean{venue="gmx"}
diff --git a/benchmarks/pm-api-latency.yml b/benchmarks/pm-api-latency.yml
index b318746a..520b30a2 100644
--- a/benchmarks/pm-api-latency.yml
+++ b/benchmarks/pm-api-latency.yml
@@ -5,7 +5,7 @@ number: "038"
title: Best prediction market API, ranked by live latency and uptime
seo_title: "Best prediction market API 2026"
seo_description: "Is the Polymarket API up? Live uptime and latency for Polymarket, Kalshi, Limitless, Manifold, Myriad, PredictIt and Smarkets price endpoints."
-subtitle: Warm price endpoint latency of seven prediction market venue APIs from three regions, plus a live uptime panel that answers whether each API is up right now.
+subtitle: Warm price endpoint latency of eight prediction market venue APIs from three regions, plus a live uptime panel that answers whether each API is up right now.
category: Trading
status: live
@@ -17,11 +17,11 @@ seo_intro: |
When the Polymarket API stops answering, the only pages that notice are
downdetector clones aggregating complaints. None of them actually probe
the API. This benchmark does. A harness in three regions requests the
- price endpoint of Polymarket, Kalshi, Limitless, Manifold and Myriad
- every 5 seconds over warm connections, records every round trip, and
- publishes two things venue status pages do not: a latency leaderboard
- ranking the best prediction market API by measured p50, and a per venue
- uptime panel computed from the same probes. If you are here to check
+ price endpoint of Polymarket, Kalshi, Limitless, Manifold, Myriad,
+ PredictIt, Smarkets and Metaculus every 5 seconds over warm connections,
+ records every round trip, and publishes two things venue status pages
+ do not: a latency leaderboard ranking the best prediction market API
+ by measured p50, and a per venue uptime panel computed from the same probes. If you are here to check
whether the Polymarket API is up right now, the uptime panel below is a
direct measurement, refreshed continuously, not a complaint counter.
If you are choosing which venue API to build on, the leaderboard ranks
@@ -41,31 +41,29 @@ abstract: |
this API up" is answered by direct measurement from three regions rather
than by user reports. Samples that fail because our pinned market
expired are classified probe_invalid and never count against the venue.
- Third party data aggregators that resell venue prices (Codex)
- are tracked separately in the pm-data-freshness bench and on
- the data feeds tab of the prediction markets hub. This page ranks the
- seven venues' own APIs only, so the leaderboard answers one clean
- question: which prediction market venue API is fastest right now.
+ This page ranks the venues' own APIs only, so the leaderboard
+ answers one clean question: which prediction market venue API is
+ fastest right now.
methodology:
- - "Price endpoint per venue. Polymarket: CLOB `/midpoint`. Kalshi: `/markets/{ticker}`. Limitless: `/markets/{slug}`. Manifold: `/v0/market/{id}`. Myriad: `/markets/{slug}`. This is the hot path of real integrations: one market, one quote, polled in a loop."
+ - "Price endpoint per venue. Polymarket: CLOB `/midpoint`. Kalshi: `/markets/{ticker}`. Limitless: `/markets/{slug}`. Manifold: `/v0/market/{id}`. Myriad: `/markets/{slug}`. PredictIt: `/api/marketdata/markets/{id}` (15s interval, 5 req/min limit). Smarkets: `/v3/markets/{id}/`. Metaculus: `/api2/questions/{id}/`. This is the hot path of real integrations: one market, one quote, polled in a loop."
- "Warm probes every 5s (7s on Manifold, whose API sits behind max-age=5 plus stale-while-revalidate=10) from us-east, eu-west and sgp over a keep alive pool. A separate cold connect probe once per minute records the TCP plus TLS handshake a brand new client pays."
- "Every sample carries a cache flag read from cf-cache-status, x-cache and age headers. Latency aggregates exclude cache hits: a CDN hit measures the nearest edge, not the venue's API, and including Manifold's residual cache hits would flatter it by design."
- "Uptime is the average of a per venue, per region health gauge (pmapi_health) computed by the harness from probe outcomes. 100 percent means every probe cycle in the window succeeded from that region. The 24h and 7d panels below are the live answer to whether each API is up."
- "Outcome classification: ok, timeout (8s), http_4xx, http_5xx, throttled, probe_invalid, net_error. probe_invalid covers failures that are our pin's fault, such as a market that resolved intraday; those are excluded from both latency and uptime. The pinned market is re selected daily at 00:00 UTC and immediately on probe_invalid."
- - "Division of labor with the sibling benches: how each venue behaves as request rates climb, including throttle onset and 429 handling, is measured in pm-rate-limits (bench 037). How fresh third party data providers relay Polymarket data is measured in pm-data-freshness (bench 032). This bench ranks the venues' own APIs on latency and uptime at a polite request rate."
+ - "Division of labor with the sibling benches: how each venue behaves as request rates climb, including throttle onset and 429 handling, is measured in pm-rate-limits (bench 037). WebSocket connect-to-snapshot and trade publication lag are measured in pm-ws-latency (bench 114). This bench ranks the venues' own APIs on latency and uptime at a polite request rate."
- "Myriad's origin is a single region US East deployment behind Heroku, so its latency from eu-west and sgp is dominated by geography. Reported as measured, called out per region."
- "All five venues are probed by the same open source harness as bench 037, from the same processes, so the two benches share one probe budget and one identifying User-Agent: `OpenChainBench/1.0 (+https://openchainbench.com/methodology; contact@openchainbench.com)`. We publish latency and uptime measurements only, never market data."
- - "Aggregators that resell venue data (Codex) are deliberately excluded from this leaderboard. They are not venue APIs, they are relays on top of venue APIs, and ranking them next to the venues they relay would compare two different products on one axis. Their freshness lag against the Polymarket CLOB T0 stream is the relevant metric, measured in the pm-data-freshness bench. The prediction markets hub data feeds tab lists each aggregator with the venues it covers."
+ - "Data aggregators that resell venue prices are deliberately excluded from this leaderboard. They are relays on top of venue APIs, and ranking them next to the venues they relay would compare two different products on one axis. The prediction markets hub data feeds tab lists each aggregator with the venues it covers."
- "Regions: us-east, eu-west, sgp (Railway). Histogram buckets 25ms to 10s."
findings:
- "{{best_name}} currently has the lowest warm price endpoint latency at {{best_p50}} p50 over the last 24h, making it the fastest prediction market API in the cohort by direct measurement."
- - "All five venue APIs typically sit above 99 percent measured uptime; the gaps that do appear are short and regional, which is exactly what a complaint based status page cannot tell you. The uptime panels show the live 24h and 7d figures per venue."
+ - "All eight venue APIs typically sit above 99 percent measured uptime; the gaps that do appear are short and regional, which is exactly what a complaint based status page cannot tell you. The uptime panels show the live 24h and 7d figures per venue."
- "{{name:manifold}} would look faster than it is if cache hits counted: its whole API sits behind a 5 second CDN cache. With cache hits excluded its origin answers at {{p50:manifold}} p50, an honest number a trading bot polling fresh quotes will actually see."
- "{{name:myriad}} serves every region from a single US East origin, so its cohort worst p50 of {{p50:myriad}} is mostly geography. Switch the region dimension to us-east to see the API itself rather than the speed of light."
- "{{name:polymarket}} answers its midpoint endpoint at {{p50:polymarket}} p50. Because we probe it every 5 seconds from three regions, the uptime panel doubles as a Polymarket API status check that updates continuously instead of waiting for user reports."
- - "For builders who consume venue data through a managed relay, the relevant question is freshness lag, not request latency. Codex is measured in the pm-data-freshness bench (T0 against the Polymarket CLOB stream). The prediction markets hub data feeds tab lists each aggregator with the venues it covers."
+ - "For builders who consume venue data through a managed relay, the relevant question is freshness lag and WebSocket latency, not REST request latency. The pm-ws-latency bench measures trade publication lag on the Polymarket CLOB WebSocket. The prediction markets hub data feeds tab lists each aggregator with the venues it covers."
disclaimer: "Uptime here means our probes succeeded from three specific regions at a polite request rate. A venue can be up for us and degraded for you, especially during regional network incidents, and a brief blip between probe cycles can go unrecorded. Treat the panel as a measured signal, not a guarantee."
@@ -83,7 +81,7 @@ faq:
- q: "Why do latency numbers exclude CDN cache hits?"
a: "Because a cache hit measures the distance to the nearest edge node, not the venue's API. Manifold serves its entire API behind a 5 second cache and Kalshi's market list sits on CloudFront, so a naive average would mostly rank CDN proximity. Every sample carries a cache flag read from the response headers, and the headline aggregates only non cached, successful requests. The number you see is what a client polling for fresh quotes actually experiences at the origin."
- q: "Does this benchmark cover throttling or data providers?"
- a: "No, deliberately. What happens to each venue's API as request rates climb, including 429 onset and queueing behaviour, is its own benchmark, pm-rate-limits, which runs a careful daily ramp. And if you consume venue data through a provider such as Codex rather than calling the venue directly, the relevant number is relay freshness, measured in pm-data-freshness. This page answers two narrower questions: which venue API is fastest at a polite request rate, and is it up right now."
+ a: "No, deliberately. What happens to each venue's API as request rates climb, including 429 onset and queueing behaviour, is its own benchmark, pm-rate-limits, which runs a careful daily ramp. WebSocket latency and trade publication lag are covered in pm-ws-latency. This page answers two narrower questions: which venue API is fastest at a polite request rate, and is it up right now."
per_chain_explainer:
- slug: polymarket
@@ -107,14 +105,16 @@ per_chain_explainer:
- slug: smarkets
h2: Smarkets API latency
body: "Smarkets is a UK-regulated peer-to-peer betting exchange with a public CLOB order book endpoint. The harness probes /v3/markets/{id}/ for price (5s interval) and /v3/markets/{id}/quotes/ for the book (5s interval). No authentication is required for either. No visible CDN layer sits between the probe and origin, so latency reflects the API server response time directly. Smarkets is the only non-US venue in the cohort and its primary user base and server infrastructure are UK-centric, which affects the relative latency from US East vs EU West probe nodes."
-
+ - slug: metaculus
+ h2: Metaculus API latency
+ body: "Metaculus is a public forecasting aggregator with no real-money markets, probed via its /api2/questions/{id}/ endpoint every 5 seconds from three regions. No authentication is required and no CDN cache layer was detected on the single-question endpoint. Metaculus has no order book and no WebSocket: all data access is REST. The uptime panel shows the live health gauge for the question API; the latency reflects the Django origin server without a CDN buffer. For builders querying aggregate community forecasts rather than market prices, Metaculus is the only venue in this cohort with a purely public read API at sub-second latency."
source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-rate-limits
prometheus:
window: 24h
expected_freshness_seconds: 300
-rank_matrix_query: 1000 * label_replace(histogram_quantile(0.50, sum by (venue, region, le) (rate(pmapi_request_duration_seconds_bucket{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets",conn="warm",class="price",cache!="hit"}[24h]))), "provider", "$1", "venue", "(.+)")
+rank_matrix_query: 1000 * label_replace(histogram_quantile(0.50, sum by (venue, region, le) (rate(pmapi_request_duration_seconds_bucket{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets|metaculus",conn="warm",class="price",cache!="hit"}[24h]))), "provider", "$1", "venue", "(.+)")
dimensions:
venue:
@@ -126,6 +126,7 @@ dimensions:
- { value: myriad, label: Myriad }
- { value: predictit, label: PredictIt }
- { value: smarkets, label: Smarkets }
+ - { value: metaculus, label: Metaculus }
region:
- { value: all, label: All regions }
- { value: us-east, label: US East }
@@ -136,21 +137,21 @@ metric_panels:
- id: uptime_24h
label: Uptime 24h
description: "Share of probe cycles that succeeded over the last 24 hours, averaged across the three probe regions. 100 percent means every 5 second probe of the venue's API came back healthy."
- metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets"}[24h]))
+ metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets|metaculus"}[24h]))
label_key: venue
unit: pct
higher_is_better: true
- id: uptime_7d
label: Uptime 7d
description: "Same health gauge averaged over 7 days. Short outages that vanish from the 24h figure stay visible here for a week."
- metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets"}[7d]))
+ metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets|metaculus"}[7d]))
label_key: venue
unit: pct
higher_is_better: true
- id: uptime_30d
label: Uptime 30d
description: "Same health gauge averaged over 30 days. Feeds the ledger's 30d window toggle so a one-hour outage that's already aged out of the 24h panel still shows up in the table over a month."
- metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets"}[30d]))
+ metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets|metaculus"}[30d]))
label_key: venue
unit: pct
higher_is_better: true
@@ -158,7 +159,7 @@ metric_panels:
- id: cold_connect_p50
label: Cold connect
description: "TCP plus TLS handshake time on the once a minute cold probe with keep alives disabled. The startup cost a brand new client pays before its first request."
- metric: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_connect_seconds_bucket{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets"}[24h])) by (le))
+ metric: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_connect_seconds_bucket{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets|metaculus"}[24h])) by (le))
label_key: venue
unit: ms
higher_is_better: false
@@ -238,20 +239,20 @@ providers:
series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="myriad",source="direct",class="price",conn="warm",cache!="hit"}[1h])) by (le))
- slug: predictit
name: PredictIt
- tag: "CFTC no-action letter, US political markets only, probed at 15s due to 5 req/min rate limit"
+ tag: "CFTC no-action letter, US political markets only, 15s probe interval (5 req/min)"
formula: "p50 of warm price requests at 15s intervals against /api/marketdata/markets/{id}, successful requests only, 24h window. Rate limit constraint means fewer samples than other venues."
queries:
- p50: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="predictit",source="direct",class="price",conn="warm"}[24h])) by (le))
- p90: 1000 * histogram_quantile(0.90, sum(rate(pmapi_request_duration_seconds_bucket{venue="predictit",source="direct",class="price",conn="warm"}[24h])) by (le))
- p99: 1000 * histogram_quantile(0.99, sum(rate(pmapi_request_duration_seconds_bucket{venue="predictit",source="direct",class="price",conn="warm"}[24h])) by (le))
- mean: 1000 * sum(rate(pmapi_request_duration_seconds_sum{venue="predictit",source="direct",class="price",conn="warm"}[24h])) / sum(rate(pmapi_request_duration_seconds_count{venue="predictit",source="direct",class="price",conn="warm"}[24h]))
+ p50: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="predictit",source="direct",class="price",conn="warm",cache!="hit"}[24h])) by (le))
+ p90: 1000 * histogram_quantile(0.90, sum(rate(pmapi_request_duration_seconds_bucket{venue="predictit",source="direct",class="price",conn="warm",cache!="hit"}[24h])) by (le))
+ p99: 1000 * histogram_quantile(0.99, sum(rate(pmapi_request_duration_seconds_bucket{venue="predictit",source="direct",class="price",conn="warm",cache!="hit"}[24h])) by (le))
+ mean: 1000 * sum(rate(pmapi_request_duration_seconds_sum{venue="predictit",source="direct",class="price",conn="warm",cache!="hit"}[24h])) / sum(rate(pmapi_request_duration_seconds_count{venue="predictit",source="direct",class="price",conn="warm",cache!="hit"}[24h]))
success: clamp_max(sum(rate(pmapi_requests_total{venue="predictit",source="direct",class="price",conn="warm",outcome="ok"}[24h])) / sum(rate(pmapi_requests_total{venue="predictit",source="direct",class="price",conn="warm",outcome!="probe_invalid"}[24h])), 1)
sample_size: sum(increase(pmapi_requests_total{venue="predictit",source="direct",class="price",conn="warm",outcome!="probe_invalid"}[24h]))
- series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="predictit",source="direct",class="price",conn="warm"}[1h])) by (le))
+ series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="predictit",source="direct",class="price",conn="warm",cache!="hit"}[1h])) by (le))
- slug: smarkets
name: Smarkets
- tag: "UK FCA-regulated CLOB exchange, public book endpoint, political and sports markets"
+ tag: "UK FCA-regulated CLOB, public book endpoint, political and sports markets"
formula: "p50 of warm price requests against /v3/markets/{id}/, successful requests only, 24h window. Origin-level responses, no CDN cache."
queries:
p50: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="smarkets",source="direct",class="price",conn="warm",cache!="hit"}[24h])) by (le))
@@ -261,3 +262,17 @@ providers:
success: clamp_max(sum(rate(pmapi_requests_total{venue="smarkets",source="direct",class="price",conn="warm",outcome="ok"}[24h])) / sum(rate(pmapi_requests_total{venue="smarkets",source="direct",class="price",conn="warm",outcome!="probe_invalid"}[24h])), 1)
sample_size: sum(increase(pmapi_requests_total{venue="smarkets",source="direct",class="price",conn="warm",outcome!="probe_invalid"}[24h]))
series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="smarkets",source="direct",class="price",conn="warm",cache!="hit"}[1h])) by (le))
+
+ - slug: metaculus
+ name: Metaculus
+ tag: "Public forecasting platform, no auth, no order book, REST /api2/questions/"
+ formula: "p50 of warm round trips against the single question endpoint /api2/questions/{id}/, successful requests only, 24h window. No CDN cache layer detected."
+ queries:
+ p50: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="metaculus",source="direct",class="price",conn="warm",cache!="hit"}[24h])) by (le))
+ p90: 1000 * histogram_quantile(0.90, sum(rate(pmapi_request_duration_seconds_bucket{venue="metaculus",source="direct",class="price",conn="warm",cache!="hit"}[24h])) by (le))
+ p99: 1000 * histogram_quantile(0.99, sum(rate(pmapi_request_duration_seconds_bucket{venue="metaculus",source="direct",class="price",conn="warm",cache!="hit"}[24h])) by (le))
+ mean: 1000 * sum(rate(pmapi_request_duration_seconds_sum{venue="metaculus",source="direct",class="price",conn="warm",cache!="hit"}[24h])) / sum(rate(pmapi_request_duration_seconds_count{venue="metaculus",source="direct",class="price",conn="warm",cache!="hit"}[24h]))
+ success: clamp_max(sum(rate(pmapi_requests_total{venue="metaculus",source="direct",class="price",conn="warm",outcome="ok"}[24h])) / sum(rate(pmapi_requests_total{venue="metaculus",source="direct",class="price",conn="warm",outcome!="probe_invalid"}[24h])), 1)
+ sample_size: sum(increase(pmapi_requests_total{venue="metaculus",source="direct",class="price",conn="warm",outcome!="probe_invalid"}[24h]))
+ series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="metaculus",source="direct",class="price",conn="warm",cache!="hit"}[1h])) by (le))
+
diff --git a/benchmarks/pm-data-freshness.yml b/benchmarks/pm-data-freshness.yml
deleted file mode 100644
index c6fb7bd1..00000000
--- a/benchmarks/pm-data-freshness.yml
+++ /dev/null
@@ -1,138 +0,0 @@
-# OpenChainBench. Bench № 032
-
-slug: pm-data-freshness
-number: "032"
-title: Fastest prediction market data API, live freshness across venues
-seo_title: "Fastest prediction market data API 2026"
-seo_description: "Fastest prediction market data API ranked live across Polymarket and Kalshi. Milliseconds Codex and Predexon lag the venue gateway publish on top markets."
-subtitle: Per event delay between provider arrival and the venue gateway publish, measured every minute on the top markets by 24 hour volume across Polymarket and Kalshi.
-
-category: Aggregators
-status: live
-metric: Freshness delta vs venue
-unit: ms
-higher_is_better: false
-
-seo_intro: |
- Prediction markets generate the most time sensitive event stream in crypto.
- An election market settles in seconds, a sports book moves on every play.
- Builders that integrate Polymarket or Kalshi through a data provider
- rather than hitting each venue directly need to know how many
- milliseconds that provider adds between when the venue itself publishes
- a trade and when the provider relays the same trade to its WebSocket
- subscribers. This benchmark measures exactly that. The harness
- subscribes to the same basket of top volume markets on each venue's own
- canonical source (T0) and on Codex GraphQL subscriptions. Each trade
- is cross correlated across the streams, and
- the per provider lag versus the venue gateway publish time is recorded
- as a Prometheus histogram. The leaderboard sorts by p50 freshness delta
- in milliseconds, lower is better. Use the venue tab at the top of the
- page to switch between Polymarket and Kalshi.
-
-abstract: |
- Three WebSocket subscribers ride the same rotating basket of ~20 top
- volume markets simultaneously on each venue. For every trade event
- published on the venue's own canonical WebSocket gateway (the T0), we
- record the moment it lands and the moment each provider relays the
- same trade. The signature used to match a trade across providers is a
- tuple based on the market identifier, price and size of the fill, and
- a small time bucket, which is robust against the minor clock skew
- between gateways. Providers that fail to relay a trade within 90
- seconds are not counted toward their p50, only toward their receive
- total, so a provider can look fresh on the leaderboard while silently
- dropping events. The success rate column flags that. Coverage today:
- Codex on Polymarket and Kalshi.
-
-methodology:
- - "Anchor rows: on each venue tab the venue itself (Polymarket, Kalshi) is the canonical T0, so its row reads as the harness network round trip, near zero by construction. It is kept on the board to document the reference point, not as a competing feed; the comparison rows are the data providers measured against it."
- - "Polymarket T0. `wss://ws-subscriptions-clob.polymarket.com/ws/market` is public, no auth, sub 50ms gateway publish latency from EU West. Cross correlation uses (conditionId, priceUSD rounded to 3 decimals, sizeUSD micros, 5s time bucket)."
- - "Kalshi T0. `https://api.elections.kalshi.com/v1/social/trades` (REST), polled every 5s with cursor pagination. Every trade carries `create_date` at microsecond precision; that timestamp is the canonical T0. Poll cadence affects only correlation timing, not the freshness number. The official Kalshi WebSocket needs RSA PSS signed headers from a US KYC account and 403s from non US IPs, not viable for a public benchmark."
- - "Codex GraphQL subscriptions. `wss://graph.codex.io/graphql` with the `graphql-transport-ws` subprotocol. Firehose `onPredictionTradesCreated` subscription filtered client side to the venue's marketIds. Covers both Polymarket and Kalshi."
- - "Basket. Top 20 active markets by 24h volume per venue, refreshed every 5 minutes. Polymarket pulls from `gamma-api.polymarket.com`, Kalshi pulls from `api.elections.kalshi.com/trade-api/v2/markets`."
- - "Histogram buckets: 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000 ms. The venue's own arrivals always land in the smallest bucket because by construction we measure other providers against it."
- - "Region. `eu-west` (Railway europe-west4). Both venue gateways are geographically distributed; deltas reflect EU client to EU gateway latency."
- - "Venue clock asymmetry. Polymarket CLOB trades carry no venue timestamp so T0 is when our harness receives the WS event; the Polymarket-to-harness RTT cancels inside the Codex delta. Kalshi's REST `create_date` is venue-side so the Codex Kalshi delta includes a Codex-to-harness RTT (50 to 100 ms) the Polymarket math cancels out. Kalshi rows are slightly stricter for the same provider, not more favorable; the gap is small versus the multi-second deltas the chart highlights."
-
-findings:
- - "Each venue's own gateway is by definition the freshest source on the leaderboard for that venue. The lag versus its own publish time is on the order of the network round trip from the harness to the gateway, typically below 100 ms p50 from EU West."
- - "{{name:codex}} sits at p50 {{p50:codex}} on the Polymarket tab because Codex indexes the chain event (Polygon block confirmation) rather than the off chain orderbook publish. The lag includes Polygon block time (~2s) plus ingestion. On Kalshi Codex ingests Kalshi's own WebSocket feed directly, so the delta is purely pipeline latency."
- - "The spread between providers reflects integration depth: native gateway vs chain indexed pipeline. None of these is wrong, they answer different questions. For live trading UIs the gateway path is the only viable one."
-
-faq:
- - q: "Which prediction market data API has the lowest latency right now?"
- a: "{{best_name}} currently leads at {{best_p50}} (p50 over the last 24h) measured as time from the venue's own canonical WebSocket publish to provider relay. The leaderboard re sorts every minute on fresh Prometheus samples, so the answer reflects the actual measured lag on the active market basket, not a marketing claim. Switch the venue tab at the top to see Polymarket vs Kalshi independently."
- - q: "What does 'freshness delta' mean for a prediction market API?"
- a: "We connect to the venue's own canonical WebSocket and to the provider's WebSocket simultaneously, subscribe to the same markets, and for every trade event we record how many milliseconds the provider takes to relay the event after the venue itself publishes it. Lower is better. The venue's own gateway publish time is the canonical T0 because by construction nothing downstream can be faster than the source."
- - q: "Why don't you include Polymarket REST polling on this benchmark?"
- a: "When the Polymarket tab is selected, freshness is a WebSocket question. REST polling at 1s would have a floor freshness around 500ms (poll interval / 2) plus RTT, dominated by how often you poll. The Polymarket gateway WebSocket exists for exactly this reason, to avoid that floor. Adding REST as a row would make the leaderboard noisy without changing the conclusion: WebSocket beats polling by definition for real time data."
- - q: "What does the Kalshi T0 source measure?"
- a: "Kalshi's own public WebSocket at `wss://external-api-ws.kalshi.com/trade-api/ws/v2`, channel `trade`. The harness reads the `msg.ts_ms` field of each fill as the canonical publish timestamp and uses it as T0 for every Kalshi side comparison. Just like Polymarket's CLOB WebSocket on the Polymarket tab, it is the source against which provider latency is measured."
- - q: "Are these numbers comparable to Kalshi or Limitless?"
- a: "Kalshi is now its own tab on this page, click the Venue selector at the top to switch. Each venue uses its own canonical WebSocket as T0, so the absolute numbers are not directly comparable across tabs (different gateways, different network paths), but the relative provider ordering within each tab is honest. Limitless is not yet covered, we will add a tab if and when meaningful provider coverage exists for it."
- - q: "How does OpenChainBench measure freshness?"
- a: "Three WebSocket clients run in parallel inside the harness, all subscribed to the same basket of top volume markets on the active venue. Every 5 minutes we refresh the basket from the venue's own markets API. For each trade event, we compute a signature based on market id, price, size and a 5 second time bucket, and record the wall clock receive time on each provider. The freshness delta is `recv_provider - recv_venue` for the same signature. We export the histogram to Prometheus, the leaderboard reads the 24h p50."
-
-source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-freshness-bench
-
-prometheus:
- window: 24h
- expected_freshness_seconds: 300
-
-# Venue selector. tabs at the top of the page. Server injects
-# `venue="X"` into every PromQL query for the active tab. There is no
-# "all" entry because cross venue averaging would mix two different
-# canonical T0 gateways, which is not a meaningful comparison; the
-# default landing tab is Polymarket (the first entry below).
-dimensions:
- venue:
- - { value: polymarket, label: Polymarket }
- - { value: kalshi, label: Kalshi }
-
-providers:
- - slug: polymarket
- name: Polymarket
- tag: Native gateway WebSocket, no auth, sub 50ms publish latency
- formula: "Polymarket gateway publish time is the canonical T0 on the Polymarket tab. The near zero row is the network round trip from the harness to the gateway, not a comparison against another source."
- queries:
- # Polymarket is T0 by construction on the Polymarket tab. Multiply
- # the 0.5 ms floor by pm_health{provider="polymarket"} so the
- # dimension framework's injected venue label gates the row:
- # pm_health{provider="polymarket", venue="polymarket"} = 1 -> 0.5,
- # pm_health{provider="polymarket", venue="kalshi"} = 0 -> 0,
- # and the leaderboard hides empty rows.
- p50: pm_health{provider="polymarket"} * 0.5
- p90: pm_health{provider="polymarket"} * 0.5
- p99: pm_health{provider="polymarket"} * 0.5
- mean: pm_health{provider="polymarket"} * 0.5
- success: avg_over_time(pm_health{provider="polymarket"}[24h])
- sample_size: sum(increase(pm_events_total{provider="polymarket"}[24h]))
- series: pm_health{provider="polymarket"} * 0.5
-
- - slug: kalshi
- name: Kalshi
- tag: Native venue REST, create_date is canonical T0
- formula: "Kalshi gateway publish time is the canonical T0 on the Kalshi tab. The near zero row is the network round trip from the harness to the Kalshi endpoint, not a comparison against another source."
- queries:
- # Symmetric to the Polymarket row, gated by pm_health{provider="kalshi"}
- # so the venue label injected at runtime zeros out the row on the
- # Polymarket tab.
- p50: pm_health{provider="kalshi"} * 0.5
- p90: pm_health{provider="kalshi"} * 0.5
- p99: pm_health{provider="kalshi"} * 0.5
- mean: pm_health{provider="kalshi"} * 0.5
- success: avg_over_time(pm_health{provider="kalshi"}[24h])
- sample_size: sum(increase(pm_events_total{provider="kalshi"}[24h]))
- series: pm_health{provider="kalshi"} * 0.5
-
- - slug: codex
- name: Codex
- tag: Chain indexer on Polymarket, native ingestion on Kalshi
- formula: "Median ms lag versus the venue gateway. On Polymarket Codex indexes the on chain Polygon settlement of each trade (block time ~2s plus ingestion). On Kalshi Codex ingests Kalshi's own WebSocket directly so the lag is pure pipeline latency."
- queries:
- p50: histogram_quantile(0.50, sum(rate(pm_freshness_delta_ms_bucket{provider="codex",kind="trade"}[24h])) by (le))
- p90: histogram_quantile(0.90, sum(rate(pm_freshness_delta_ms_bucket{provider="codex",kind="trade"}[24h])) by (le))
- p99: histogram_quantile(0.99, sum(rate(pm_freshness_delta_ms_bucket{provider="codex",kind="trade"}[24h])) by (le))
- mean: sum(rate(pm_freshness_delta_ms_sum{provider="codex",kind="trade"}[24h])) / sum(rate(pm_freshness_delta_ms_count{provider="codex",kind="trade"}[24h]))
- success: clamp_max(sum(rate(pm_matched_total{provider="codex",kind="trade"}[24h])) / sum(rate(pm_events_total{provider="polymarket",kind="trade"}[24h])), 1)
- sample_size: sum(increase(pm_matched_total{provider="codex",kind="trade"}[24h]))
- series: histogram_quantile(0.50, sum(rate(pm_freshness_delta_ms_bucket{provider="codex",kind="trade"}[1h])) by (le))
diff --git a/benchmarks/pm-fee-comparison.yml b/benchmarks/pm-fee-comparison.yml
deleted file mode 100644
index 42b34b35..00000000
--- a/benchmarks/pm-fee-comparison.yml
+++ /dev/null
@@ -1,74 +0,0 @@
-# OpenChainBench. Bench 112
-
-slug: pm-fee-comparison
-number: "112"
-title: Prediction market fees, Polymarket vs Kalshi and more
-seo_title: "Prediction market fees 2026: Polymarket, Kalshi, Limitless compared"
-seo_description: "What does it cost to trade on Polymarket, Kalshi, Limitless and Myriad? Maker and taker fees, protocol cuts, total cost to trade, sourced from each venue's published fee schedule."
-subtitle: Maker and taker fees, protocol cuts and net cost to trade across five prediction market venues, sourced from official fee schedules and verified from fills data.
-
-category: Trading
-status: draft
-metric: Effective taker fee
-unit: pct
-higher_is_better: false
-
-seo_intro: |
- Nobody publishes a clean side-by-side of what it actually costs to trade prediction markets. Polymarket charges a flat percentage fee on each trade. Kalshi charges per-contract on a published schedule. Limitless embeds a fee in the AMM spread. Manifold uses play money. Myriad varies by market type. This bench parses each venue's fills data and official fee schedule so the comparison is based on measured values rather than marketing copy.
-
-abstract: |
- Prediction market venues charge fees in ways that resist direct comparison. Polymarket charges 2% of
- potential payout on every trade, regardless of contract price. Kalshi charges a per-contract fee on
- a published schedule that scales with contract price and is capped at extremes, making near-certainty
- contracts very cheap in percentage terms. Limitless charges fees through its CLOB spread. Manifold
- uses play-money (mana) and has no real-money fee. Myriad charges a protocol fee on settlement. This
- bench normalizes all fee structures to effective basis points of notional, measured at three
- representative contract price points: at-the-money (50 cents), near-certainty (5 cents or 95 cents),
- and mid-skewed (30 cents). The resulting leaderboard answers which venue is cheapest for a given
- market condition, not just which venue has the lowest headline number.
-
-methodology:
- - "Fee normalization: all fees are expressed as basis points of gross notional (contract price times contract count times $1 resolution value) at three price points: $0.50 (at-the-money), $0.05/$0.95 (near-certainty), and $0.30/$0.70 (mid-skewed). This converts Kalshi's per-contract dollar fee and Polymarket's payout-percentage fee to a common unit."
- - "Polymarket fee measurement: the 2% fee is deducted from potential payout. A YES share bought at $0.50 pays $0.98 on resolution, not $1.00. Effective fee in basis points = 200 at all price points because the fee percentage is flat. No maker rebate exists on the public CLOB."
- - "Kalshi fee measurement: Kalshi publishes a tiered per-contract fee schedule. Fees are capped at low and high price extremes. The harness reads the published schedule and computes the fee at each price point as fee/contract_price in basis points. Near-certainty markets benefit most from the cap."
- - "Limitless and Myriad fee measurement: CLOB spread is used as a proxy for effective fee where explicit fee schedules are not published. The half-spread on the pinned market at each price tier is recorded as the effective round-trip cost."
- - "Manifold is play-money and carries no real-money fee. It is included in the cohort for completeness but its fee row reads zero and it is excluded from the real-money fee rankings."
- - "Data cadence: fee schedules are polled once per hour. Per-contract fees on Kalshi are re-read whenever the published schedule changes (detected via hash of the fee schedule endpoint). Effective fee rows on the leaderboard update within 1 hour of any venue publishing a fee change."
-
-findings:
- - "Polymarket charges a flat 2% fee on each trade, one of the more transparent structures in the cohort since the rate does not vary by market or counterparty type."
- - "Kalshi's per-contract fee means the effective percentage cost depends on where the contract trades relative to par. Fees are highest in percentage terms on near-certainty markets."
- - "Manifold is play-money only. No real capital is at risk and no real fee is charged."
-
-disclaimer: "Fee schedules change without notice. Verify current fees directly with each venue before trading. This bench measures published and observed fees; it is not an endorsement of any venue."
-
-source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-fee-comparison
-
-faq:
- - q: "What is the Polymarket trading fee?"
- a: "Polymarket charges a 2% fee on each trade, deducted from the potential payout. If you buy YES shares at $0.50, each share pays out $0.98 rather than $1.00 if you win. The fee applies to both sides of the trade. There is no maker rebate on the public CLOB at the time of writing."
- - q: "What is the Kalshi trading fee?"
- a: "Kalshi charges a per-contract fee that scales with the contract's settlement price. The fee schedule is published on Kalshi's documentation. At mid-range prices ($0.40 to $0.60 per contract) the fee is typically around $0.07 per contract. Fees are capped, which makes very-low-price or very-high-price contracts cheaper in percentage terms."
- - q: "Is Polymarket or Kalshi cheaper to trade?"
- a: "It depends on the market. Polymarket's flat 2% fee is predictable. Kalshi's per-contract fee is cheaper on near-certainty markets where the cap kicks in, and more expensive on mid-book markets. The live fee comparison will show side-by-side numbers once the harness is running."
-
-providers:
- - slug: polymarket
- name: Polymarket
- tag: 2% fee on potential payout, no maker rebate, flat across all price points
-
- - slug: kalshi
- name: Kalshi
- tag: Per-contract fee on published schedule, capped at price extremes, US-regulated
-
- - slug: limitless
- name: Limitless
- tag: CLOB venue, fee structure via spread, no published fee schedule
-
- - slug: manifold
- name: Manifold
- tag: Play-money (mana), no real-money fee, included for completeness
-
- - slug: myriad
- name: Myriad
- tag: Protocol fee on settlement, single-region US East
diff --git a/benchmarks/pm-geographic-access.yml b/benchmarks/pm-geographic-access.yml
deleted file mode 100644
index 8950a03c..00000000
--- a/benchmarks/pm-geographic-access.yml
+++ /dev/null
@@ -1,63 +0,0 @@
-# OpenChainBench. Bench 113
-
-slug: pm-geographic-access
-number: "113"
-status: draft
-title: Prediction market geographic access, which countries can reach each venue
-subtitle: HTTP probes from regional nodes checking 200 OK vs blocked or redirect for Polymarket, Kalshi, Limitless, Manifold and Myriad, reported as percent of regions returning a successful response.
-category: Trading
-metric: Geographic accessibility rate
-unit: pct
-higher_is_better: true
-
-abstract: |
- Prediction market venues operate under different regulatory regimes and enforce access restrictions
- at the HTTP layer. Polymarket's international exchange has geoblocked US IP addresses since a 2022
- CFTC settlement. Polymarket US (QCX), a separate CFTC-regulated entity, launched in December 2025
- and is accessible to US users. Kalshi is CFTC-regulated and accessible to US users without
- restriction. Minnesota enacted a law banning prediction markets effective August 1, 2026, which is
- being challenged in court by the CFTC. This bench probes each venue's market listing endpoint from
- nodes in 12 regions (US East, US West, EU West, EU Central, UK, Canada, Singapore, Japan, Australia,
- Brazil, India, UAE) and records the HTTP response code. A 200 OK counts as accessible; a 403,
- redirect to a block page, or connection reset counts as blocked. The result is expressed as the
- percentage of probe regions returning a successful response. The bench does not assess legal access
- rights, only observable HTTP-level behavior.
-
-methodology:
- - "Probe design: the harness sends an HTTP GET to each venue's public market listing endpoint from dedicated probe nodes in 12 geographic regions once every 15 minutes. The request carries an identifying User-Agent and no VPN or proxy. Response codes, redirect chains and response body patterns are recorded."
- - "Blocked classification: a response is classified as blocked if the HTTP status code is 403, if the response is a redirect to a URL containing terms such as block, geo, unavailable, or restricted, or if the TCP connection is reset before a response is received. Timeouts over 10 seconds are classified as unreachable rather than blocked."
- - "Metric definition: the pct metric is the share of probe regions returning a 200 OK response to the venue's market listing endpoint in the trailing 24-hour window. A venue accessible from all 12 regions scores 100 pct. A venue blocked from all US probes but accessible from 9 of 12 regions scores 75 pct."
- - "Polymarket disambiguation: the harness probes both polymarket.com (international exchange, geoblocked for US IPs) and the Polymarket US (QCX) endpoint separately and reports them as distinct rows. The combined accessibility row shows the union of regions that can reach either endpoint."
- - "Legal access vs HTTP access: this bench measures what the API returns to an HTTP request, not what a person in a given jurisdiction is legally permitted to do. A 200 OK from a US probe on a venue's endpoint does not mean US users are legally permitted to trade on that venue. Consult venue terms of service and qualified legal counsel for legal access questions."
- - "Regulatory events: when a regulatory event changes HTTP-level access (such as the Polymarket US geoblock in 2022 or the Minnesota ban in 2026), the probe series will reflect the change within one probe cycle (15 minutes). The bench changelog records the date and description of each regime change."
-
-findings:
- - "Polymarket's international exchange (polymarket.com) geoblocks US IP addresses following a 2022 CFTC settlement in which Polymarket LLC paid a $1.4M fine. US probes receive a 403 or redirect. The international exchange scores below 100 pct by design on this bench."
- - "Polymarket US (QCX), a separate CFTC-regulated entity, launched December 3, 2025 and is accessible to US users. It initially launched iOS-only with a waitlist; the waitlist dropped in May 2026."
- - "Kalshi (Kalshi Exchange LLC) is CFTC-regulated and accessible to US users from all US probe regions. It is the primary regulated US-accessible real-money prediction market."
- - "Minnesota enacted a law banning prediction markets effective August 1, 2026. The CFTC is challenging this law in court. The bench monitors Minnesota-region probes for HTTP-level changes as the legal challenge progresses."
-
-disclaimer: "Geographic access status changes as regulatory environments evolve. Verify current access directly with each venue before depositing funds. This bench measures API-level HTTP responses, not legal access rights."
-
-source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-geographic-access
-
-providers:
- - slug: polymarket
- name: Polymarket
- tag: International exchange geoblocked for US IPs since 2022 CFTC settlement
-
- - slug: kalshi
- name: Kalshi
- tag: CFTC-regulated, accessible to US users, all regions
-
- - slug: limitless
- name: Limitless
- tag: CLOB venue, no documented geographic restrictions
-
- - slug: manifold
- name: Manifold
- tag: Play-money, US-accessible, no geographic restrictions
-
- - slug: myriad
- name: Myriad
- tag: Single-region US East, no documented geographic restrictions
diff --git a/benchmarks/pm-rate-limits.yml b/benchmarks/pm-rate-limits.yml
index 27e41b86..aa25ca11 100644
--- a/benchmarks/pm-rate-limits.yml
+++ b/benchmarks/pm-rate-limits.yml
@@ -4,8 +4,8 @@ slug: pm-rate-limits
number: "037"
title: Prediction market API rate limits, tested with a daily ramp
seo_title: "Prediction market API rate limits 2026"
-seo_description: "Prediction market API rate limits measured live: daily request ramp against Polymarket, Kalshi, Limitless, Manifold, Myriad."
-subtitle: Warm latency on book, price and list endpoints of five prediction market venue APIs, plus a daily rate limit ramp that records added latency and throttle onset per request tier.
+seo_description: "Prediction market API rate limits measured live: daily request ramp against Polymarket, Kalshi, Limitless, Manifold, Myriad, PredictIt, Smarkets and Metaculus."
+subtitle: Warm latency on book, price and list endpoints of eight prediction market venue APIs, plus a daily rate limit ramp that records added latency and throttle onset per request tier.
category: Trading
status: live
@@ -49,13 +49,13 @@ abstract: |
second window.
methodology:
- - "Endpoint classes per venue. Polymarket: CLOB `/book`, `/midpoint`, Gamma `/markets`. Kalshi: `/markets/{ticker}/orderbook`, `/markets/{ticker}`, `/markets`. Limitless: `/markets/{slug}/orderbook`, `/markets/{slug}`, `/markets/active`. Manifold: `/v0/bets`, `/v0/market/{id}`, `/v0/markets`. Myriad: `/markets/{slug}`, `/markets`. Myriad exposes no order book endpoint; that absence is reported, not worked around."
+ - "Endpoint classes per venue. Polymarket: CLOB `/book`, `/midpoint`, Gamma `/markets`. Kalshi: `/markets/{ticker}/orderbook`, `/markets/{ticker}`, `/markets`. Limitless: `/markets/{slug}/orderbook`, `/markets/{slug}`, `/markets/active`. Manifold: `/v0/bets`, `/v0/market/{id}`, `/v0/markets`. Myriad, PredictIt, Metaculus: price and list only (no order book). Smarkets: `/v3/markets/{id}/quotes/`, `/v3/markets/{id}/`, `/v3/events/`."
- "Pinned market per venue: most liquid, near the money (price between 0.15 and 0.85), expiring more than 24h out. Re pinned daily at 00:00 UTC and immediately on probe_invalid. A degenerate 0.99 book has a latency profile that does not represent real usage."
- "Warm probes every 5s (7s on Manifold, whose entire API sits behind max-age=5 plus stale-while-revalidate=10; tighter spacing would only measure the cache). Cold connect probe once per minute with keep alives disabled, TCP plus TLS handshake recorded separately."
- "Every sample records a cache flag from cf-cache-status, x-cache and age headers. Kalshi's market list is served from CloudFront with max-age=15, so its list latency measures the edge, not the API. Edge served samples (cache=hit) are excluded from every latency aggregate and reported through the cache label, so no venue ranks on its CDN."
- "Outcome classification: ok, timeout (8s), http_4xx, http_5xx, throttled (429), probe_invalid, net_error. Latency aggregates include successful requests only; failures count toward the success rate. probe_invalid covers stale pins, e.g. Limitless serves a CDN cached 400 for four hours once a market expires. Those are our fault, never the venue's."
- "Daily ramp: 60s per tier at rising requests per 10s window against the book endpoint, one venue at a time, one region at a time on disjoint UTC hours (02:00 us-east, 04:00 eu-west, 06:00 sgp)."
- - "Ramp tier clamps: Polymarket and Kalshi 25/50/100 per 10s (at most 7 percent of Polymarket's documented 1500 per 10s book budget), Manifold 15/30/60 (500 req/min per IP documented), Limitless 10/20/40 (limits undocumented, so conservative). Kalshi stops at the first 429 since its token bucket is documented. Myriad is excluded: its keyless budget is 30 req per 10s and ramping it would only measure our own quota."
+ - "Ramp tiers: Polymarket + Kalshi 25/50/100 per 10s, Manifold 15/30/60, Limitless 10/20/40, Smarkets 10/25/50, Metaculus 5/10/20. Kalshi stops at first 429 (documented token bucket). Myriad excluded (keyless budget 30/10s). PredictIt excluded (5 req/min published limit)."
- "Ramp abort guard: the run stops the moment throttled plus 5xx responses exceed 1 percent of any 10 second window. The headline ramp metric is p50 latency during the tier minus the warm baseline p50 of the previous hour in the same region, because venues behind Cloudflare queue bursts without ever returning 429, and added latency is the only honest throttle signal there."
- "WebSocket panel covers venues with a public market WebSocket, which today means Polymarket only. Kalshi requires authentication for WS, Manifold and Myriad have none, Limitless uses socket.io. Absence is shown, not penalized."
- "Myriad's origin is a single region US East deployment behind Heroku. Latency from eu-west and sgp is geography, reported as measured."
@@ -79,7 +79,7 @@ faq:
- q: "What is the Kalshi API rate limit?"
a: "Kalshi documents tiered rate limits per access level, with the basic read tier around 20 requests per second at the time of writing. It is the only venue in this cohort with a documented token bucket. Our ramp stops at the first 429 on Kalshi out of respect for that documented contract, and the tier where it happens (or does not) is recorded in the ramp panel."
- q: "Which prediction market has the fastest API?"
- a: "{{best_name}} currently has the lowest warm book endpoint latency at {{best_p50}} (p50 over the last 24h). Note what this does and does not measure: it is the latency of the venue's own public API over a warm connection at a polite request rate, per region. It is not data freshness via third party providers, which is a separate benchmark (pm-data-freshness)."
+ a: "{{best_name}} currently has the lowest warm book endpoint latency at {{best_p50}} (p50 over the last 24h). Note what this does and does not measure: it is the latency of the venue's own public API over a warm connection at a polite request rate, per region. For data freshness and WebSocket latency, see the pm-api-latency and pm-ws-latency benches."
- q: "Does Manifold have an API rate limit?"
a: "Yes, 500 requests per minute per IP, documented, and Manifold explicitly welcomes bots as long as they stay on a single IP. Be aware that the whole API is served behind a cache with max-age=5 and stale-while-revalidate=10, so polling faster than every 6 seconds mostly returns cached responses. Our probes space out to 7 seconds per URL and label every sample that still comes back from cache."
- q: "What are the Limitless API rate limits?"
@@ -87,7 +87,7 @@ faq:
- q: "How do you test rate limits without abusing the APIs?"
a: "Four guardrails. Tiers stay well inside documented budgets where they exist (at most 7 percent of Polymarket's book allowance). Every run aborts as soon as throttled plus server errors exceed 1 percent of a 10 second window, and Kalshi stops at the first 429. Only one region ramps a venue at a time, on disjoint UTC hours. And every request carries an identifying User-Agent with a contact address, so a venue can reach us or filter us selectively. The measurement is throttle onset, not stress to failure."
- q: "Why measure the venue APIs directly instead of a data provider?"
- a: "They answer different questions. If you build directly on a venue, its native API latency and throttle behaviour set your floor, and that is what this benchmark measures. If you consume the venue through a data provider such as Codex or Predexon, what matters is how fresh the provider's relay is, which is measured separately in the pm-data-freshness benchmark. Both pages link each other so you can compare the paths."
+ a: "They answer different questions. If you build directly on a venue, its native API latency and throttle behaviour set your floor, and that is what this benchmark measures. For API latency at a polite rate, see pm-api-latency. For WebSocket connect and trade publication lag, see pm-ws-latency."
per_chain_explainer:
- slug: polymarket
@@ -111,14 +111,16 @@ per_chain_explainer:
- slug: smarkets
h2: Smarkets rate limits
body: "Smarkets showed no visible rate limiting during reconnaissance probes at up to 6 rapid requests per second. The daily ramp uses conservative tiers (10, 25, 50 requests per 10 seconds) with the standard abort guard. The CLOB quotes endpoint returns full bid-offer depth for the pinned market on every request. Unlike Polymarket which queues bursts under Cloudflare, Smarkets returns direct origin responses, so the ramp records actual latency scaling rather than queue inflation."
-
+ - slug: metaculus
+ h2: Metaculus rate limits
+ body: "Metaculus does not document API rate limits. The harness uses the most conservative ramp in the cohort (5/10/20 requests per 10 seconds) with the standard abort guard. There is no order book endpoint and no WebSocket. Because Metaculus is a forecasting aggregator rather than a betting exchange, its API is designed for low-frequency access; polling faster than once per 5 seconds on a single question likely hits undocumented limits. Warm price endpoint p50: {{p50:metaculus}}."
source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-rate-limits
prometheus:
window: 24h
expected_freshness_seconds: 300
-rank_matrix_query: 1000 * label_replace(histogram_quantile(0.50, sum by (venue, region, le) (rate(pmapi_request_duration_seconds_bucket{cache!="hit",conn="warm",class="book"}[24h]) or rate(pmapi_request_duration_seconds_bucket{cache!="hit",conn="warm",class="price",venue=~"myriad|predictit"}[24h]))), "provider", "$1", "venue", "(.+)")
+rank_matrix_query: 1000 * label_replace(histogram_quantile(0.50, sum by (venue, region, le) (rate(pmapi_request_duration_seconds_bucket{cache!="hit",conn="warm",class="book"}[24h]) or rate(pmapi_request_duration_seconds_bucket{cache!="hit",conn="warm",class="price",venue=~"myriad|predictit|metaculus"}[24h]))), "provider", "$1", "venue", "(.+)")
dimensions:
venue:
@@ -130,6 +132,7 @@ dimensions:
- { value: myriad, label: Myriad }
- { value: predictit, label: PredictIt }
- { value: smarkets, label: Smarkets }
+ - { value: metaculus, label: Metaculus }
region:
- { value: all, label: All regions }
- { value: us-east, label: US East }
@@ -140,7 +143,7 @@ metric_panels:
- id: ramp_added_latency
label: Ramp added latency
description: "Worst tier of the daily ramp: p50 latency during the burst minus the warm baseline p50 of the previous hour, same region. Venues that queue under load instead of returning 429 show up here."
- metric: 1000 * max(max_over_time(pmapi_ramp_added_latency_seconds{}[24h]))
+ metric: 1000 * max(max_over_time(pmapi_ramp_added_latency_seconds{venue!="predictit",venue!="myriad"}[24h]))
label_key: venue
unit: ms
higher_is_better: false
@@ -231,7 +234,7 @@ providers:
- slug: myriad
name: Myriad
- tag: No order book endpoint, no WebSocket, single region Heroku origin, keyless 30 req/10s
+ tag: No order book, no WebSocket, single region Heroku origin, keyless 30 req/10s
formula: "p50 of warm full round trips against the single market endpoint (Myriad exposes no order book, so the price class is its hot path), successful requests only, 24h window. Cross region latency reflects the single US East origin."
queries:
p50: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="myriad",class="price",conn="warm"}[24h])) by (le))
@@ -267,3 +270,17 @@ providers:
success: clamp_max(sum(rate(pmapi_requests_total{venue="smarkets",class="book",conn="warm",outcome="ok"}[24h])) / sum(rate(pmapi_requests_total{venue="smarkets",class="book",conn="warm",outcome!="probe_invalid"}[24h])), 1)
sample_size: sum(increase(pmapi_requests_total{venue="smarkets",class="book",conn="warm",outcome!="probe_invalid"}[24h]))
series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="smarkets",class="book",conn="warm"}[1h])) by (le))
+
+ - slug: metaculus
+ name: Metaculus
+ tag: "Public forecasting platform, no auth, no book endpoint, ramp 5/10/20 per 10s"
+ formula: "p50 of warm price requests against /api2/questions/{id}/, successful requests only, 24h window. No book endpoint. Ramp at 5/10/20 per 10s (limits undocumented)."
+ queries:
+ p50: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="metaculus",class="price",conn="warm"}[24h])) by (le))
+ p90: 1000 * histogram_quantile(0.90, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="metaculus",class="price",conn="warm"}[24h])) by (le))
+ p99: 1000 * histogram_quantile(0.99, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="metaculus",class="price",conn="warm"}[24h])) by (le))
+ mean: 1000 * sum(rate(pmapi_request_duration_seconds_sum{cache!="hit",venue="metaculus",class="price",conn="warm"}[24h])) / sum(rate(pmapi_request_duration_seconds_count{cache!="hit",venue="metaculus",class="price",conn="warm"}[24h]))
+ success: clamp_max(sum(rate(pmapi_requests_total{venue="metaculus",class="price",conn="warm",outcome="ok"}[24h])) / sum(rate(pmapi_requests_total{venue="metaculus",class="price",conn="warm",outcome!="probe_invalid"}[24h])), 1)
+ sample_size: sum(increase(pmapi_requests_total{venue="metaculus",class="price",conn="warm",outcome!="probe_invalid"}[24h]))
+ series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="metaculus",class="price",conn="warm"}[1h])) by (le))
+
diff --git a/benchmarks/pm-resolution-delay.yml b/benchmarks/pm-resolution-delay.yml
new file mode 100644
index 00000000..2ee3b434
--- /dev/null
+++ b/benchmarks/pm-resolution-delay.yml
@@ -0,0 +1,243 @@
+# OpenChainBench. Bench № 039
+
+slug: pm-resolution-delay
+number: "039"
+title: Resolution delay, Polymarket vs Kalshi
+seo_title: "Prediction market resolution delay live 2026"
+seo_description: "Polymarket and Kalshi resolution time measured live: median delay from close to payout, broken down by category. Polymarket anchored on UMA ProposePrice, Kalshi on settlement_ts from the public API."
+subtitle: "How long from market close to final settlement: Polymarket measured onchain via UMA ProposePrice to QuestionResolved, Kalshi via settlement_ts minus close_time from the public REST API. Auto-generated multivariate parlays excluded from Kalshi data."
+
+category: Trading
+status: live
+metric: Resolution delay
+unit: s
+higher_is_better: false
+
+seo_intro: |
+ The most asked question about prediction markets after "is it legal" is "how
+ long until I get paid". The answer circulating on social media for Polymarket
+ is that 93 percent of markets resolve within 2 hours. That number has no
+ published methodology and cannot be reproduced from Polymarket's own Gamma
+ API. This benchmark measures what can actually be observed: for Polymarket,
+ the time from the first onchain outcome proposal on UMA's optimistic oracle to
+ the onchain resolution, read directly from Polygon logs. For Kalshi, the time
+ from a market's scheduled close_time to the first poll at which its result is
+ non-null, measured every five minutes. Both venues are broken down by category
+ so the comparison is apples to apples.
+
+abstract: |
+ Polymarket: a listener follows the UMA CTF adapters on Polygon
+ (QuestionInitialized, QuestionResolved, QuestionReset) and the Optimistic
+ Oracle they call (ProposePrice, DisputePrice). Delay = first ProposePrice
+ block to QuestionResolved block, joined to Gamma for category tags.
+
+ Kalshi: a poller hits the public /trade-api/v2/markets?status=settled endpoint
+ every 5 minutes. Delay = settlement_ts - close_time (both from API, exact,
+ no polling error). Categories resolved via /events/{event_ticker}. Auto-generated
+ multivariate parlay markets (Kalshi "Exotics" category) are excluded: they
+ settle algorithmically in seconds and are not prediction market questions. No auth required.
+
+methodology:
+ - "Polymarket anchor: the first OO ProposePrice block timestamp, not Gamma fields. Verified live: Gamma closedTime equals the QuestionResolved block timestamp (circular by construction) and endDate is a scheduled buffer most markets beat."
+ - "Polymarket sources: Polygon logs of the post-migration UMA CTF adapters (binary 0x65070BE9..., neg-risk 0x69c47De9...) and their Optimistic Oracle (0x2c0367a9...). Gamma supplies the market catalogue and category tags."
+ - "Polymarket join: questionID = keccak256 of the proposal ancillary data, verified against QuestionInitialized events."
+ - "Kalshi anchor: settlement_ts from the public markets API (exact timestamp, no polling error). Delay = settlement_ts - close_time. close_time is the scheduled market close, not the real-world event end."
+ - "Kalshi scope: traditional prediction markets only. Exotics (auto-generated multivariate parlays) are excluded by design: they auto-settle so fast they never appear in status=closed, so the watchlist only contains real market questions. Categories from /events/{event_ticker} (lazy cache): Sports→sports, Politics/Elections→politics, Crypto/Financials→crypto, everything else→other."
+ - "Anchor asymmetry: Polymarket's 2h UMA challenge window is protocol-enforced: no market can resolve in under 2h on the clean path. Kalshi has no such floor; traditional sports and politics markets settle in minutes to hours after close_time. The difference in distributions reflects both operational speed and architecture."
+ - "Categories: sports, politics, crypto, other, consistent across venues. Per-category rows are the honest unit of comparison because market types run different resolution windows."
+ - "Disputes (Polymarket only): a DisputePrice before resolution marks the market disputed. Disputed delays land in the disputed series and are excluded from the main category rows."
+ - "Pending backlog (Polymarket only): markets past their scheduled endDate with no onchain resolution, polled from Gamma every cycle."
+ - "Listener: 45s incremental Polygon log polling with a 7-day chunked backfill on restart. Kalshi poller ticks every 5 min. Both coexist in the same process."
+
+findings:
+ - "Polymarket across all markets: median onchain resolution delay {{p50:polymarket-all}} from first proposal to resolution, p99 at {{p99:polymarket-all}}. The within 2 hours share is in the table above, measured, and does not match the 93 percent figure that circulates without a methodology."
+ - "Kalshi across all markets (excluding auto-generated parlays): median resolution delay {{p50:kalshi-all}} from scheduled close_time to settlement_ts. Kalshi resolves internally: no onchain oracle, no mandatory challenge window. The difference vs Polymarket's median reflects both operational speed and Polymarket's protocol-enforced 2h minimum."
+ - "Categories are not comparable on a single median: sports, crypto and politics markets run different resolution processes on each venue."
+ - "Disputes (Polymarket only) are rare but expensive: disputed markets carry materially longer delays than the clean path."
+
+disclaimer: "Polymarket delay is measured from the first onchain outcome proposal to resolution (UMA challenge window, minimum 2h by protocol). Kalshi delay is settlement_ts - close_time (exact from API), where close_time is the scheduled market close, not the real-world event end. Kalshi auto-generated parlay markets (Exotics) are excluded. Both anchors are lower bounds on total user wait from the real-world event ending."
+
+faq:
+ - q: "How long does Polymarket take to pay out after a market ends?"
+ a: "Measured onchain, the median delay from the first outcome proposal to resolution is currently {{p50:polymarket-all}} across all markets, and redemptions are claimable immediately after resolution. The full distribution matters: p99 is {{p99:polymarket-all}}, and the per category rows show that sports, crypto and politics markets run on different verification windows. Add the time between the real world event ending and the first proposal, which is not measurable onchain."
+ - q: "How long does Kalshi take to resolve?"
+ a: "Kalshi resolves via its own internal process, not through an onchain oracle. The delay measured here is from a market's scheduled close_time to the first API poll (every 5 minutes) at which a result appears. The current median is {{p50:kalshi-all}}. This reflects Kalshi's operational resolution speed, not an onchain settlement time."
+ - q: "Do 93 percent of Polymarket markets resolve within 2 hours?"
+ a: "We could not reproduce that figure. The fields in Polymarket's public Gamma API cannot produce it: closedTime is written at the moment of resolution (circular) and endDate is a scheduled buffer most markets beat. Measured from the first onchain outcome proposal, the within 2 hours share is in the table above and has been materially below 93 percent since this benchmark started."
+ - q: "What happens when a Polymarket market is disputed?"
+ a: "A dispute on UMA's optimistic oracle resets the question and routes it through UMA token holder voting, replacing the normal verification window with a voting period measured in days. The disputed label separates those markets so clean path percentiles stay honest."
+ - q: "Why is my Polymarket market still unresolved?"
+ a: "Three common reasons: the underlying event has not concluded despite the scheduled end date; no one has submitted a proposal yet (the clock only starts at first proposal); or the market is in a UMA dispute, which adds days. The pending backlog gauge counts all markets past their end date without onchain resolution."
+
+per_chain_explainer:
+ - slug: polymarket-all
+ h2: Polymarket resolution — all markets
+ body: "All Polymarket markets combined, measured from first UMA ProposePrice to QuestionResolved on Polygon. The median is {{p50:polymarket-all}}. Sports dominates volume; crypto runs fastest; politics carries conservative verification windows. p99 at {{p99:polymarket-all}} reflects the long tail of disputes and postponed events."
+ - slug: polymarket-sports
+ h2: Polymarket sports resolution
+ body: "Sports markets dominate Polymarket volume and currently resolve at {{p50:polymarket-sports}} median from first proposal. Nearly all disputes happen here: contested endings and stat corrections make sports the category where the oracle's verification window earns its keep."
+ - slug: polymarket-politics
+ h2: Polymarket politics resolution
+ body: "Politics markets resolve at {{p50:polymarket-politics}} median, the slow end. Political outcomes often need an authoritative source to publish before anyone proposes, and verification windows are conservative because these markets carry the largest open interest."
+ - slug: polymarket-crypto
+ h2: Polymarket crypto resolution
+ body: "Crypto price markets resolve at {{p50:polymarket-crypto}} median, the fastest Polymarket category. The outcome is machine checkable against price feeds the moment the window closes, so proposals arrive promptly and short verification windows suffice."
+ - slug: kalshi-all
+ h2: Kalshi resolution — all markets
+ body: "All Kalshi markets combined, measured from scheduled close_time to API-observed finalization (poll every 5 min). The median is {{p50:kalshi-all}}. Kalshi resolves via internal processes, not an onchain oracle, so the anchor is the market's scheduled close rather than an onchain proposal."
+ - slug: kalshi-sports
+ h2: Kalshi sports resolution
+ body: "Kalshi sports markets resolve at {{p50:kalshi-sports}} median from scheduled close. Sports outcomes can require official stat verification, so delays vary with league reporting speed."
+ - slug: kalshi-politics
+ h2: Kalshi politics resolution
+ body: "Kalshi politics markets resolve at {{p50:kalshi-politics}} median from scheduled close. Political outcomes on Kalshi are resolved by Kalshi's internal operations team against authoritative sources."
+ - slug: kalshi-crypto
+ h2: Kalshi crypto resolution
+ body: "Kalshi crypto markets resolve at {{p50:kalshi-crypto}} median from scheduled close. Machine-checkable outcomes against price feeds allow for prompt resolution."
+
+source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-resolution-delay
+
+prometheus:
+ window: 24h
+ expected_freshness_seconds: 900
+ freshness_metric: pmres_resolutions_total
+
+metric_panels:
+ - id: pending_backlog
+ label: Pending backlog (Polymarket)
+ description: "Polymarket markets past their scheduled end date with no onchain resolution yet. The queue a waiting bettor experiences; oldest entries are usually postponed events, not oracle failures."
+ metric: sum(pmres_pending_markets{})
+ label_key: category
+ unit: count
+ higher_is_better: false
+ - id: disputes
+ label: Disputes (Polymarket)
+ description: "Polymarket resolutions that went through UMA's dispute process since the listener's backfill window. Rare, concentrated in sports."
+ metric: sum(pmres_disputes_total{})
+ label_key: category
+ unit: count
+ higher_is_better: false
+
+ledger_columns:
+ - { label: "p50 (median delay)", slot: p50 }
+ - { label: "p99", slot: p99 }
+ - { label: "Pending", panel: pending_backlog }
+ - { label: "Disputes", panel: disputes }
+
+providers:
+ - slug: polymarket-all
+ name: "Polymarket — All"
+ tag: Every resolved Polymarket market across categories
+ formula: "Median seconds from first OO ProposePrice block to QuestionResolved block on Polygon. Success = share resolved within 2 hours of proposal."
+ queries:
+ p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket"}))
+ p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket"}))
+ p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket"}))
+ mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="polymarket"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket"})
+ success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="polymarket",le="7200"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket"}), 1)
+ sample_size: sum(pmres_resolutions_total{venue="polymarket"})
+ series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket"}))
+
+ - slug: polymarket-sports
+ name: "Polymarket — Sports"
+ tag: Game and match markets, the volume majority
+ formula: "Median seconds from first proposal to resolution for Polymarket sports markets."
+ queries:
+ p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="sports"}))
+ p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="sports"}))
+ p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="sports"}))
+ mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="polymarket",category="sports"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="sports"})
+ success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="polymarket",le="7200",category="sports"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="sports"}), 1)
+ sample_size: sum(pmres_resolutions_total{venue="polymarket",category="sports"})
+ series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="sports"}))
+
+ - slug: polymarket-crypto
+ name: "Polymarket — Crypto"
+ tag: Price markets, machine checkable outcomes
+ formula: "Median seconds from first proposal to resolution for Polymarket crypto price markets."
+ queries:
+ p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="crypto"}))
+ p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="crypto"}))
+ p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="crypto"}))
+ mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="polymarket",category="crypto"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="crypto"})
+ success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="polymarket",le="7200",category="crypto"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="crypto"}), 1)
+ sample_size: sum(pmres_resolutions_total{venue="polymarket",category="crypto"})
+ series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="crypto"}))
+
+ - slug: polymarket-politics
+ name: "Polymarket — Politics"
+ tag: Election and policy markets, conservative windows
+ formula: "Median seconds from first proposal to resolution for Polymarket politics markets."
+ queries:
+ p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="politics"}))
+ p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="politics"}))
+ p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="politics"}))
+ mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="polymarket",category="politics"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="politics"})
+ success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="polymarket",le="7200",category="politics"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="politics"}), 1)
+ sample_size: sum(pmres_resolutions_total{venue="polymarket",category="politics"})
+ series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="politics"}))
+
+ - slug: polymarket-other
+ name: "Polymarket — Other"
+ tag: Culture, weather, science and everything else
+ formula: "Median seconds from first proposal to resolution for uncategorized Polymarket markets."
+ queries:
+ p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="other"}))
+ p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="other"}))
+ p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="other"}))
+ mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="polymarket",category="other"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="other"})
+ success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="polymarket",le="7200",category="other"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="other"}), 1)
+ sample_size: sum(pmres_resolutions_total{venue="polymarket",category="other"})
+ series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="other"}))
+
+ - slug: kalshi-all
+ name: "Kalshi — All"
+ tag: All Kalshi markets, close_time to observed finalization
+ formula: "Median seconds from Kalshi market close_time to first API poll observing a non-null result (5 min polling interval). Success = share resolved within 2 hours of close."
+ queries:
+ p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi"}))
+ p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi"}))
+ p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi"}))
+ mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="kalshi"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi"})
+ success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="kalshi",le="7200"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi"}), 1)
+ sample_size: sum(pmres_resolutions_total{venue="kalshi"})
+ series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi"}))
+
+ - slug: kalshi-sports
+ name: "Kalshi — Sports"
+ tag: Kalshi sports markets
+ formula: "Median seconds from Kalshi sports market close_time to observed finalization."
+ queries:
+ p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="sports"}))
+ p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="sports"}))
+ p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="sports"}))
+ mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="kalshi",category="sports"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi",category="sports"})
+ success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="kalshi",le="7200",category="sports"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi",category="sports"}), 1)
+ sample_size: sum(pmres_resolutions_total{venue="kalshi",category="sports"})
+ series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="sports"}))
+
+ - slug: kalshi-politics
+ name: "Kalshi — Politics"
+ tag: Kalshi politics markets
+ formula: "Median seconds from Kalshi politics market close_time to observed finalization."
+ queries:
+ p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="politics"}))
+ p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="politics"}))
+ p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="politics"}))
+ mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="kalshi",category="politics"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi",category="politics"})
+ success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="kalshi",le="7200",category="politics"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi",category="politics"}), 1)
+ sample_size: sum(pmres_resolutions_total{venue="kalshi",category="politics"})
+ series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="politics"}))
+
+ - slug: kalshi-crypto
+ name: "Kalshi — Crypto"
+ tag: Kalshi crypto markets
+ formula: "Median seconds from Kalshi crypto market close_time to observed finalization."
+ queries:
+ p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="crypto"}))
+ p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="crypto"}))
+ p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="crypto"}))
+ mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="kalshi",category="crypto"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi",category="crypto"})
+ success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="kalshi",le="7200",category="crypto"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi",category="crypto"}), 1)
+ sample_size: sum(pmres_resolutions_total{venue="kalshi",category="crypto"})
+ series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="crypto"}))
diff --git a/benchmarks/pm-ws-latency.yml b/benchmarks/pm-ws-latency.yml
index 992b388f..f66c26de 100644
--- a/benchmarks/pm-ws-latency.yml
+++ b/benchmarks/pm-ws-latency.yml
@@ -5,7 +5,7 @@ number: "114"
title: Polymarket WebSocket latency, live connection and update speed from 3 regions
seo_title: "Polymarket WebSocket latency 2026: connection time and update speed"
seo_description: "How fast does the Polymarket WebSocket connect and deliver market updates? Live p50 from us-east, eu-west and Singapore measured continuously by OpenChainBench."
-subtitle: Time from WebSocket dial to first market snapshot plus inter-message update rate on the Polymarket CLOB feed, probed from three regions 24 hours a day.
+subtitle: Connect-to-snapshot latency, inter-message update rate, and trade publication lag on the Polymarket WebSocket feed (ranked) and Kalshi WebSocket (measured, not ranked), probed from three regions 24 hours a day.
category: Trading
status: live
@@ -47,12 +47,12 @@ methodology:
- "Regions: us-east (Railway us-east4), eu-west (Railway europe-west4), sgp (Railway asia-southeast1). The Polymarket CLOB WS endpoint is globally distributed via Cloudflare, so regional differences reflect edge proximity and routing, not server capacity."
- "Keepalive: a background goroutine sends the literal text PING every 10 seconds and the venue responds with PONG. Sessions that stop receiving data are detected by the reconnect logic on the next PING timeout. Disconnect rate is exported as a counter."
- "Pin: the harness targets the same market used by the pm-rate-limits bench (highest 24h volume binary market with last trade price between 0.15 and 0.85), refreshed daily at 00:00 UTC. The WS client re-subscribes automatically when the pin changes."
- - "Metrics: pmapi_ws_connect_to_snapshot_seconds histogram and pmapi_ws_update_interarrival_seconds histogram, both labeled by venue, region and source. The same pm-rate-limits harness process that probes REST endpoints runs the WS session, so both REST and WS data share one Railway deployment per region."
- - "Other venues: Kalshi WS (wss://external-api-ws.kalshi.com) requires RSA-PSS signed headers from a US KYC account and returns 403 from non-US IPs, making it not viable for a public three-region benchmark. Limitless uses socket.io over HTTP long-polling, not standard WebSocket. Manifold, Myriad, PredictIt and Smarkets have no public WebSocket endpoint."
+ - "Metrics: pmapi_ws_connect_to_snapshot_seconds and pmapi_ws_update_interarrival_seconds histograms, labeled by venue, region and source. The pm-rate-limits harness runs the WS session alongside REST probes, sharing one Railway deployment per region. Kalshi note: its ticker channel is delta-only (no snapshot on subscribe), so connect-to-snapshot measures time to the first price change, which depends on market activity."
+ - "Other venues: Kalshi WS (wss://api.elections.kalshi.com/trade-api/ws/v2) requires RSA-PSS signed headers from a US KYC account and returns 403 from non-US IPs, so it is measured from us-east only and excluded from the rank matrix. Limitless uses socket.io over HTTP long-polling, not standard WebSocket. Manifold, Myriad, PredictIt, Smarkets and Metaculus have no public WebSocket endpoint."
findings:
- - "Polymarket's CLOB WebSocket connects and delivers the first snapshot in {{p50:polymarket}} p50 (24h, all regions). This is the startup cost a new subscriber pays to go from a fresh dial to live market data."
- - "Kalshi's WebSocket is probed from us-east only (RSA-PSS auth required, 403 from non-US IPs) and records {{p50:kalshi}} p50 connect-to-snapshot from the east US region. Its inter-arrival and disconnect metrics are in the panels below."
+ - "Polymarket's CLOB WebSocket connects and delivers the first orderbook snapshot in {{p50:polymarket}} p50 (24h, all regions). This is the startup cost a new subscriber pays to go from a fresh dial to live market data."
+ - "Kalshi's WebSocket ticker channel is delta-only: it does not send a market snapshot on subscription. The 'connect-to-snapshot' timing for Kalshi measures the time from dial to the first incremental price update, not to a full snapshot. On an active market this is typically 10 to 60s and reflects market activity level as much as connection speed. The inter-arrival and disconnect metrics below are more representative of the real-time feed health."
- "Inter-arrival time on the feed (the steady-state update rate on an active market) is published as a metric panel below for both venues. The faster the inter-arrival, the more granular the real-time picture a subscriber gets."
disclaimer: "Connect-to-snapshot includes Cloudflare edge latency and routing from each probe region. Numbers vary with network conditions and market activity. This bench measures observed end-to-end startup time, not venue-side processing time alone."
@@ -75,7 +75,7 @@ prometheus:
window: 24h
expected_freshness_seconds: 120
-rank_matrix_query: 1000 * label_replace(histogram_quantile(0.50, sum by (venue, region, le) (rate(pmapi_ws_connect_to_snapshot_seconds_bucket{source="direct",venue=~"polymarket|kalshi"}[24h]))), "provider", "$1", "venue", "(.+)")
+rank_matrix_query: 1000 * label_replace(histogram_quantile(0.50, sum by (venue, region, le) (rate(pmapi_ws_connect_to_snapshot_seconds_bucket{source="direct",venue="polymarket"}[24h]))), "provider", "$1", "venue", "(.+)")
dimensions:
region:
@@ -99,6 +99,20 @@ metric_panels:
label_key: venue
unit: ms
higher_is_better: false
+ - id: trade_pub_lag_p50
+ label: Trade pub lag p50
+ description: "Median delay between venue server-side trade creation and WS subscriber receipt. Polymarket: CLOB WS timestamp field vs receive time (all regions). Kalshi: REST create_date vs WS ticker receive time (us-east only). Lower is better."
+ metric: 1000 * histogram_quantile(0.50, sum by (venue) (rate(pmapi_ws_trade_pub_lag_seconds_bucket{source="direct",venue=~"polymarket|kalshi"}[24h])))
+ label_key: venue
+ unit: ms
+ higher_is_better: false
+ - id: trade_pub_lag_p90
+ label: Trade pub lag p90
+ description: "90th percentile trade publication lag. The tail cost a subscriber pays waiting for the venue to push a confirmed trade. Kalshi us-east only."
+ metric: 1000 * histogram_quantile(0.90, sum by (venue) (rate(pmapi_ws_trade_pub_lag_seconds_bucket{source="direct",venue=~"polymarket|kalshi"}[24h])))
+ label_key: venue
+ unit: ms
+ higher_is_better: false
- id: disconnect_rate
label: Disconnects 24h
description: "Number of WebSocket disconnects recorded across all regions in the last 24 hours. Kalshi probes us-east only; Polymarket probes all three regions."
@@ -110,7 +124,7 @@ metric_panels:
providers:
- slug: polymarket
name: Polymarket
- tag: Public no-auth CLOB WebSocket, globally distributed via Cloudflare, PING keepalive, 3 regions
+ tag: Public no-auth CLOB WebSocket, Cloudflare CDN, PING keepalive, 3 regions
formula: "p50 of connect-to-snapshot time across all three regions (24h). Covers dial, TLS, HTTP upgrade and initial snapshot delivery."
queries:
p50: 1000 * histogram_quantile(0.50, sum(rate(pmapi_ws_connect_to_snapshot_seconds_bucket{source="direct",venue="polymarket"}[24h])) by (le))
@@ -123,8 +137,8 @@ providers:
- slug: kalshi
name: Kalshi
- tag: RSA-PSS auth required, CFTC-regulated, probed from us-east only (403 from non-US IPs)
- formula: "p50 of connect-to-snapshot time from us-east only (24h). Kalshi WS returns 403 from eu-west and sgp even with valid credentials, so cross-region comparison is not available."
+ tag: RSA-PSS auth required, CFTC-regulated, us-east only (403 from non-US IPs)
+ formula: "p50 of time-to-first-price-update from us-east only (24h). Kalshi's ticker channel is delta-only (no snapshot on subscribe), so this measures time from dial to the next price change — not an initial snapshot delivery."
queries:
p50: 1000 * histogram_quantile(0.50, sum(rate(pmapi_ws_connect_to_snapshot_seconds_bucket{source="direct",venue="kalshi",region="us-east"}[24h])) by (le))
p90: 1000 * histogram_quantile(0.90, sum(rate(pmapi_ws_connect_to_snapshot_seconds_bucket{source="direct",venue="kalshi",region="us-east"}[24h])) by (le))
@@ -140,7 +154,7 @@ per_chain_explainer:
body: "Polymarket publishes a public CLOB WebSocket at wss://ws-subscriptions-clob.polymarket.com/ws/market with no authentication required. After subscribing with an assets_ids array, the feed delivers orderbook snapshots and price change events. The harness records connect-to-snapshot at {{p50:polymarket}} p50 (24h, all regions) and the inter-arrival distribution visible in the panels below. The feed is fronted by Cloudflare so regional latency differences are small. The PING keepalive every 10 seconds keeps idle connections alive. This is the only venue in the cohort with a standard, public, unauthenticated WebSocket."
- slug: kalshi
h2: Kalshi WebSocket
- body: "Kalshi's WebSocket API at wss://api.elections.kalshi.com/trade-api/ws/v2 requires RSA-PSS-SHA256 signed authentication headers (KALSHI-ACCESS-KEY, KALSHI-ACCESS-TIMESTAMP, KALSHI-ACCESS-SIGNATURE) and a US IP address. The us-east probe node connects with a keyed account and records connect-to-snapshot and inter-arrival metrics. The eu-west and sgp probe nodes return 403 even with valid credentials, so Kalshi's latency in the rank matrix reflects us-east only. The connect-to-snapshot number shown for Kalshi is therefore comparable to Polymarket's us-east column, not its global average."
+ body: "Kalshi's WebSocket API at wss://api.elections.kalshi.com/trade-api/ws/v2 requires RSA-PSS-SHA256 signed authentication headers (KALSHI-ACCESS-KEY, KALSHI-ACCESS-TIMESTAMP, KALSHI-ACCESS-SIGNATURE) and a US IP address. The us-east probe node connects with a keyed account and records connect-to-snapshot and inter-arrival metrics. The eu-west and sgp probe nodes return 403 even with valid credentials, so Kalshi's latency in the rank matrix reflects us-east only. Important: Kalshi's ticker channel does not push a market snapshot on subscribe — it sends incremental price updates only. The 'connect-to-snapshot' metric for Kalshi therefore measures how long after subscribing the next price change arrives, which is driven by market activity rather than connection speed. On an active market this is 10–60s; on a quiet market it can be several minutes. The inter-arrival panel is the more meaningful indicator of the feed's real-time responsiveness."
- slug: limitless
h2: Limitless
body: "Limitless does not expose a standard WebSocket. Its real-time channel uses socket.io, which runs over HTTP long-polling with a custom framing protocol that is not directly comparable to a native WebSocket dial-to-snapshot measurement. Limitless REST latency is in the pm-api-latency bench."
@@ -156,3 +170,6 @@ per_chain_explainer:
- slug: smarkets
h2: Smarkets
body: "Smarkets has no public WebSocket endpoint. The REST API at api.smarkets.com/v3 provides order book access without authentication. Smarkets REST latency is tracked in pm-api-latency."
+ - slug: metaculus
+ h2: Metaculus
+ body: "Metaculus has no WebSocket endpoint. It is a public forecasting platform accessed entirely via REST (/api2/questions/). For REST latency and rate limit measurements, see pm-api-latency and pm-rate-limits."
diff --git a/benchmarks/polymarket-resolution-delay.yml b/benchmarks/polymarket-resolution-delay.yml
deleted file mode 100644
index 9c058e1c..00000000
--- a/benchmarks/polymarket-resolution-delay.yml
+++ /dev/null
@@ -1,190 +0,0 @@
-# OpenChainBench. Bench № 039
-
-slug: polymarket-resolution-delay
-number: "039"
-title: Polymarket resolution time, measured onchain
-seo_title: "Polymarket resolution delay live 2026"
-seo_description: "Polymarket resolution time live from Polygon: median delay from outcome proposal to onchain payout, sports vs politics vs crypto."
-subtitle: Seconds from the first onchain outcome proposal to UMA resolution, measured per category from Polygon logs, with dispute rate and the live backlog of unresolved markets.
-
-category: Trading
-status: live
-metric: Resolution delay
-unit: s
-higher_is_better: false
-
-seo_intro: |
- The most asked question about Polymarket after "is it legal" is "how long
- until I get paid". The answer circulating on social media and repeated by
- language models is that 93 percent of markets resolve within 2 hours. That
- number has no published methodology, and the data Polymarket's own Gamma
- API exposes cannot produce it: the closedTime field is written at the
- moment of resolution (so measuring from it always yields zero) and endDate
- is a scheduled buffer that most markets beat. This benchmark measures the
- delay that can actually be observed: the time from the first onchain
- outcome proposal on UMA's optimistic oracle to the onchain resolution of
- the market, read directly from Polygon logs of Polymarket's UMA CTF
- adapters. The table breaks the delay down by category, because sports,
- crypto and politics markets run different verification windows, and a
- single global median hides that. Dispute rate and the live backlog of
- closed but unresolved markets are reported alongside.
-
-abstract: |
- A listener follows Polymarket's UMA CTF adapters on Polygon
- (QuestionInitialized, QuestionResolved, QuestionReset) and the
- Optimistic Oracle they call (ProposePrice, DisputePrice). For every
- resolved market, the recorded delay is the wall-clock time between the
- block containing the first outcome proposal and the block containing the
- resolution. Markets are joined to Polymarket's Gamma catalogue by
- questionID (the keccak hash of the proposal's ancillary data, verified
- against live events) and classified into sports, politics, crypto or
- other. A dispute resets the question and routes it through UMA's
- dispute process; those markets are counted separately so the dispute
- penalty is visible instead of polluting the happy path percentiles. The
- pending gauge counts markets past their scheduled end date that have not
- resolved onchain yet, which is the backlog a bettor waiting for a payout
- actually experiences.
-
-methodology:
- - "Anchor: the first OO ProposePrice block timestamp, not Gamma fields. Verified live: Gamma closedTime equals the QuestionResolved block timestamp to the second (circular by construction) and endDate is a scheduled buffer that a majority of markets resolve before. Any 'resolves within X' claim computed from those fields is unfalsifiable."
- - "Sources: Polygon logs of the post-migration UMA CTF adapters (binary 0x65070BE9..., neg-risk 0x69c47De9...) and their Optimistic Oracle (0x2c0367a9...), all address-configurable. Gamma supplies the market catalogue and category tags."
- - "Join: questionID = keccak256 of the proposal ancillary data, verified against QuestionInitialized events. Neg-risk markets expose a different Gamma questionID; categories fall back to keyword classification for those, the delay itself is unaffected."
- - "Categories: sports, politics, crypto, other, classified from Gamma tags plus keyword fallback. Categories run different UMA verification windows, so per category rows are the honest unit of comparison."
- - "Disputes: a DisputePrice before resolution marks the market disputed and its delay lands in the disputed series. QuestionReset events are tracked so re-proposals do not create duplicate joins."
- - "Pending backlog: markets past their scheduled endDate with no onchain resolution, polled from Gamma every cycle. This includes markets whose underlying event has not happened yet despite the date passing, so it is an upper bound."
- - "Listener: 45s incremental Polygon log polling with a 7 day chunked backfill on restart, two public RPC endpoints with rotation and backoff. Counters re-count the backfill window after a restart; quantiles are computed on the cumulative histogram, which the backfill repopulates within minutes."
- - "Delays are measured between block timestamps, so the resolution granularity is Polygon's block time (~2s), negligible against delays measured in minutes and hours."
- - "What this does not measure: the gap between the real world event ending and someone submitting the first proposal. That interval is not observable onchain; the proposal is the first onchain signal that the outcome is known. Reported delays are therefore a lower bound on the total wait."
-
-findings:
- - "Across all markets, the median onchain resolution delay is {{p50:all-markets}} from first proposal to resolution, with p99 at {{p99:all-markets}}. The within 2 hours share is on the table above, measured, and it does not match the 93 percent figure that circulates without a methodology."
- - "Categories are not comparable on a single median: sports markets currently sit at {{p50:sports}}, crypto at {{p50:crypto}}, politics at {{p50:politics}}. Different market types run different UMA verification windows, which is why the leaderboard is per category."
- - "Disputes are rare but expensive: the dispute counter on this page tracks how many resolutions went through UMA's dispute process, and disputed markets carry materially longer delays than the clean path."
- - "The pending backlog gauge is the number a waiting bettor actually feels: markets past their scheduled end date with no onchain resolution yet. Its oldest entries are weeks old, usually markets whose underlying event was postponed rather than oracle failures."
-
-disclaimer: "Delay is measured from the first onchain outcome proposal, not from the real world event ending. The gap between event and proposal is not observable onchain, so total wait times experienced by users are at least the figures shown."
-
-faq:
- - q: "How long does Polymarket take to pay out after a market ends?"
- a: "Measured onchain, the median delay from the first outcome proposal to resolution is currently {{p50:all-markets}} across all markets, and redemptions are claimable immediately after resolution. The full distribution matters though: p99 is {{p99:all-markets}}, and the per category rows above show that sports, crypto and politics markets run on different verification windows. Add the time between the real world event ending and the first proposal, which happens offchain and is not measurable, so treat these figures as the floor."
- - q: "Do 93 percent of Polymarket markets resolve within 2 hours?"
- a: "We could not reproduce that figure, and the fields in Polymarket's public Gamma API cannot produce it: closedTime is written at the moment of resolution, so measuring from it always returns zero, and endDate is a scheduled buffer most markets beat. Measured from the first onchain outcome proposal to resolution on Polygon, the within 2 hours share is shown live in the table above and has been materially below 93 percent since this benchmark started. If you have a methodology that supports the 93 percent claim, we would genuinely like to see it."
- - q: "What happens when a Polymarket market is disputed?"
- a: "A dispute on UMA's optimistic oracle resets the question and routes it through the dispute process, where UMA token holders vote. That replaces the normal verification window with a voting period measured in days. Disputes are rare, the live dispute counter on this page tracks the measured rate, but when one hits a market you are waiting on, the delay moves from minutes or hours to days. The disputed label in our data separates those markets so the clean path percentiles stay honest."
- - q: "Why is my Polymarket market still unresolved?"
- a: "Three common reasons, in decreasing likelihood. First, the underlying event has not actually concluded, even if the market's scheduled end date passed; postponed games and extended deadlines dominate the pending backlog we track. Second, no one has submitted an outcome proposal to the oracle yet; the clock we measure only starts at the first proposal. Third, the market is in a UMA dispute, which adds days. The pending backlog gauge above counts all markets past their end date without onchain resolution."
- - q: "How does Polymarket resolution actually work onchain?"
- a: "Polymarket markets resolve through UMA's optimistic oracle on Polygon. Someone posts a bond and proposes the outcome (ProposePrice). A verification window follows during which anyone can dispute by posting a matching bond. If no dispute arrives, the proposal settles and Polymarket's UMA CTF adapter resolves the market (QuestionResolved), making winning shares redeemable. If a dispute arrives, UMA token holders vote. This benchmark reads exactly those events from Polygon logs, so the delays shown are the protocol's own timestamps, not estimates."
- - q: "Which Polymarket categories resolve fastest?"
- a: "The leaderboard above ranks categories by measured median delay, and the ordering is stable enough to plan around: crypto markets, which resolve against onchain or exchange prices, run the shortest verification windows, while politics markets sit at the long end. Sports land in between and dominate volume. The numbers update live, so check the table rather than trusting a snapshot quoted in an article, including this one."
-
-per_chain_explainer:
- - slug: sports
- h2: Polymarket sports resolution
- body: "Sports markets dominate Polymarket's resolution volume and currently resolve at {{p50:sports}} median from first proposal. They are also where nearly all disputes happen: contested endings and stat corrections make sports the category where the oracle's verification window earns its keep. If you bet game markets, the practical wait is this row plus however long the league takes to make the result official, which happens before the onchain clock starts."
- - slug: politics
- h2: Polymarket politics resolution
- body: "Politics markets resolve at {{p50:politics}} median from first proposal, the slow end of the cohort. Political outcomes often need an authoritative source to publish before anyone proposes, and verification windows are conservative because these markets carry the largest open interest. The headline 'Polymarket resolves in minutes' claims circulating online are sports and crypto numbers; this row is the honest expectation for election style markets."
- - slug: crypto
- h2: Polymarket crypto resolution
- body: "Crypto price markets resolve at {{p50:crypto}} median, the fastest category. The outcome is machine checkable against price feeds the moment the window closes, so proposals arrive promptly and short verification windows suffice. High frequency up or down markets recycle continuously, which is why crypto contributes a large share of total resolutions despite modest open interest per market."
-
-source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-resolution-delay
-
-prometheus:
- window: 24h
- expected_freshness_seconds: 900
- freshness_metric: pmres_resolutions_total
-
-metric_panels:
- - id: pending_backlog
- label: Pending backlog
- description: "Markets past their scheduled end date with no onchain resolution yet. The queue a waiting bettor experiences; oldest entries are usually postponed events, not oracle failures."
- metric: sum(pmres_pending_markets{})
- label_key: category
- unit: count
- higher_is_better: false
- - id: disputes
- label: Disputes
- description: "Resolutions that went through UMA's dispute process since the listener's backfill window. Rare, and concentrated in sports."
- metric: sum(pmres_disputes_total{})
- label_key: category
- unit: count
- higher_is_better: false
-
-# Surface the two LIVE signals in the ledger so the page reads as a UMA
-# state dashboard, not just a historical characterization. The p50 is
-# statistically stable (cumulative histogram), so the live answers are
-# what shifts day to day: how many markets are stuck waiting, and how
-# many have hit UMA's dispute process.
-ledger_columns:
- - { label: "p50 (median delay)", slot: p50 }
- - { label: "p99", slot: p99 }
- - { label: "Pending", panel: pending_backlog }
- - { label: "Disputes", panel: disputes }
-
-providers:
- - slug: all-markets
- name: All markets
- tag: Every resolved market across categories
- formula: "Median seconds from first OO ProposePrice block to QuestionResolved block, cumulative histogram over the listener's window. The success column is the share resolved within 2 hours of proposal, the number the 93 percent claim pretends to be."
- queries:
- p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket))
- p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket))
- p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket))
- mean: 1000 * sum(pmres_resolution_delay_seconds_sum) / sum(pmres_resolution_delay_seconds_count)
- success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{le="7200"}) / sum(pmres_resolution_delay_seconds_count), 1)
- sample_size: sum(pmres_resolutions_total)
- series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket))
-
- - slug: sports
- name: Sports
- tag: Game and match markets, the volume majority
- formula: "Median seconds from first proposal to resolution for sports markets. Success column is the share within 2 hours of proposal."
- queries:
- p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="sports"}))
- p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{category="sports"}))
- p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{category="sports"}))
- mean: 1000 * sum(pmres_resolution_delay_seconds_sum{category="sports"}) / sum(pmres_resolution_delay_seconds_count{category="sports"})
- success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{le="7200",category="sports"}) / sum(pmres_resolution_delay_seconds_count{category="sports"}), 1)
- sample_size: sum(pmres_resolutions_total{category="sports"})
- series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="sports"}))
-
- - slug: crypto
- name: Crypto
- tag: Price markets, machine checkable outcomes
- formula: "Median seconds from first proposal to resolution for crypto price markets. Success column is the share within 2 hours of proposal."
- queries:
- p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="crypto"}))
- p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{category="crypto"}))
- p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{category="crypto"}))
- mean: 1000 * sum(pmres_resolution_delay_seconds_sum{category="crypto"}) / sum(pmres_resolution_delay_seconds_count{category="crypto"})
- success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{le="7200",category="crypto"}) / sum(pmres_resolution_delay_seconds_count{category="crypto"}), 1)
- sample_size: sum(pmres_resolutions_total{category="crypto"})
- series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="crypto"}))
-
- - slug: politics
- name: Politics
- tag: Election and policy markets, conservative windows
- formula: "Median seconds from first proposal to resolution for politics markets. Success column is the share within 2 hours of proposal."
- queries:
- p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="politics"}))
- p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{category="politics"}))
- p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{category="politics"}))
- mean: 1000 * sum(pmres_resolution_delay_seconds_sum{category="politics"}) / sum(pmres_resolution_delay_seconds_count{category="politics"})
- success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{le="7200",category="politics"}) / sum(pmres_resolution_delay_seconds_count{category="politics"}), 1)
- sample_size: sum(pmres_resolutions_total{category="politics"})
- series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="politics"}))
-
- - slug: other
- name: Other
- tag: Culture, weather, science and everything else
- formula: "Median seconds from first proposal to resolution for uncategorized markets. Success column is the share within 2 hours of proposal."
- queries:
- p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="other"}))
- p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{category="other"}))
- p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{category="other"}))
- mean: 1000 * sum(pmres_resolution_delay_seconds_sum{category="other"}) / sum(pmres_resolution_delay_seconds_count{category="other"})
- success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{le="7200",category="other"}) / sum(pmres_resolution_delay_seconds_count{category="other"}), 1)
- sample_size: sum(pmres_resolutions_total{category="other"})
- series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="other"}))
diff --git a/benchmarks/swellchain-rpc.yml b/benchmarks/swellchain-rpc.yml
new file mode 100644
index 00000000..be5e4490
--- /dev/null
+++ b/benchmarks/swellchain-rpc.yml
@@ -0,0 +1,136 @@
+# OpenChainBench. Bench № 128
+
+slug: swellchain-rpc
+number: "128"
+title: Fastest free Swellchain RPC, live no-key EVM endpoint latency
+seo_title: "Fastest free Swellchain RPC 2026"
+seo_description: "{{best_name}} leads free Swellchain RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h). 3 no-key providers measured every 60s from 3 regions."
+subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public Swellchain EVM endpoint, audited every 60 seconds from 3 regions.
+
+category: RPCs
+status: live
+metric: RPC latency
+unit: ms
+higher_is_better: false
+
+seo_intro: |
+ Swellchain is an OP Stack L2 (chain 1923) operated by Swell Network, the restaking-native rollup built on EigenLayer, ETH gas token, ~2 s block cadence, blob calldata settlement on Ethereum. The 2026-08-02 audit confirms 3 clean keyless providers: Ankr (rpc.ankr.com/swell), Sentio (swell-mainnet.rpc.sentio.xyz), and Thirdweb (1923.rpc.thirdweb.com). Excluded: official alt.technology endpoint (401 Unauthorized without key), dRPC (no swell route), Alchemy (key required).
+
+abstract: |
+ Per-chain member of the RPC latency cluster, extended to Swellchain.
+ We measure the round-trip latency of a single, identical JSON-RPC call
+ (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public
+ Swellchain endpoint that sustains continuous probing, 3 providers
+ at launch, every 60 seconds, from us-east, eu-west and Singapore. The
+ harness classifies every response (ok / http_err / jsonrpc_err / stale
+ / timeout) with a Swellchain-scaled staleness gap (20 blocks
+ at Swellchain's ~2 s block time). The cross-chain view
+ lives on the parent `rpc-capabilities` benchmark; this page is the
+ Swellchain-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: `{\"jsonrpc\":\"2.0\",\"id\":,\"method\":\"eth_getBlockByNumber\",\"params\":[\"latest\", false]}`. Plain HTTP POST, identical for every endpoint, no API key in any request. Non-cacheable by design: the rotating id defeats edge caches that would answer a bare head query without touching a node."
+ - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours."
+ - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal."
+ - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/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=\"swellchain\"`. Provider coverage at launch: 3 no-key endpoints."
+
+findings:
+ - "{{best_name}} currently leads free Swellchain RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers."
+
+faq:
+ - q: "What is the fastest free Swellchain RPC right now?"
+ a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment."
+ - q: "Which Swellchain RPCs work without an API key?"
+ a: "3 endpoints sustain continuous keyless probing at launch: Ankr (rpc.ankr.com/swell), Sentio (swell-mainnet.rpc.sentio.xyz), and Thirdweb (1923.rpc.thirdweb.com). The official Swell alt.technology endpoint requires authorization. Every listed endpoint was live-verified with a `eth_getBlockByNumber` POST returning a parsable response before inclusion."
+ - q: "Does the fastest Swellchain RPC change by region?"
+ a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate."
+ - q: "How is Swellchain RPC latency measured here, technically?"
+ a: "One identical `eth_getBlockByNumber(\"latest\", false)` POST every 60 seconds against each provider from each of 3 regions, using the same plain HTTP client the rest of the RPC cluster uses. Wall-clock round-trip is recorded at millisecond precision; p50/p90/p99 are computed via Prometheus `quantile_over_time` over 24 hours. Responses are classified (`ok` / `http_err` / `jsonrpc_err` / `stale` / `timeout`) so an endpoint stuck on an old head or returning errors behind HTTP 200 is never ranked as fastest."
+
+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="swellchain"})
+
+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: ankr
+ name: Ankr
+ tag: Ankr public RPC gateway for Swellchain
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Ankr's no-key swellchain RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="swellchain"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="ankr", chain="swellchain"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="ankr", chain="swellchain"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="ankr", chain="swellchain"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="ankr", chain="swellchain"}) / sum(ocb:rpc_call:rate_24h{provider="ankr", chain="swellchain"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="ankr", chain="swellchain"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="swellchain"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="swellchain", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="swellchain", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="swellchain", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="swellchain", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="swellchain", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="swellchain", region="sgp"}[1h])
+
+ - slug: sentio
+ name: Sentio
+ tag: Sentio RPC node for Swellchain
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Sentio's no-key swellchain RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="sentio", chain="swellchain"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="sentio", chain="swellchain"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="sentio", chain="swellchain"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="sentio", chain="swellchain"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="sentio", chain="swellchain"}) / sum(ocb:rpc_call:rate_24h{provider="sentio", chain="swellchain"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="sentio", chain="swellchain"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="sentio", chain="swellchain"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="sentio", chain="swellchain", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="sentio", chain="swellchain", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="sentio", chain="swellchain", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="sentio", chain="swellchain", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="sentio", chain="swellchain", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="sentio", chain="swellchain", region="sgp"}[1h])
+
+ - slug: thirdweb
+ name: Thirdweb
+ tag: Thirdweb public RPC gateway for Swellchain
+ formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Thirdweb's no-key swellchain RPC endpoint."
+ queries:
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="swellchain"})
+ p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="thirdweb", chain="swellchain"})
+ p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="thirdweb", chain="swellchain"})
+ mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="thirdweb", chain="swellchain"})
+ success: sum(ocb:rpc_call:ok_rate_24h{provider="thirdweb", chain="swellchain"}) / sum(ocb:rpc_call:rate_24h{provider="thirdweb", chain="swellchain"})
+ sample_size: sum(ocb:rpc_call:increase_24h{provider="thirdweb", chain="swellchain"})
+ series: avg(avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="swellchain"}[1h]))
+ regions:
+ - region: us-east
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="swellchain", region="us-east"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="swellchain", region="us-east"}[1h])
+ - region: eu-west
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="swellchain", region="eu-west"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="swellchain", region="eu-west"}[1h])
+ - region: ap-southeast
+ p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="swellchain", region="sgp"})
+ series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="swellchain", region="sgp"}[1h])
diff --git a/benchmarks/validator-yield-hyperliquid.yml b/benchmarks/validator-yield-hyperliquid.yml
new file mode 100644
index 00000000..8285722b
--- /dev/null
+++ b/benchmarks/validator-yield-hyperliquid.yml
@@ -0,0 +1,447 @@
+# OpenChainBench. Bench № 115
+
+slug: validator-yield-hyperliquid
+number: "115"
+title: "Hyperliquid validator leaderboard: live net APR, uptime, commission"
+seo_title: "Best Hyperliquid validators 2026: yield, commission, uptime ranked"
+seo_description: "Live Hyperliquid validator leaderboard ranked by net APR. All 34 validators tracked: net yield in bps, commission rate, uptime %, delegated stake in USD. Updated every 5 minutes from /info validatorSummaries."
+subtitle: Per-validator net yield (predictedApr.day × uptime) in basis points across all 34 Hyperliquid validators. Ranked by net APR; commission and uptime shown as secondary columns.
+category: Blockchains
+status: live
+metric: Net yield
+unit: bps
+higher_is_better: true
+
+disclaimer: |
+ net_yield = predictedApr.day x uptimeFraction from /info validatorSummaries. No MEV layer at the validator level on Hyperliquid; the centralised sequencer captures order-flow value upstream. Jailed validators land at 0 bps regardless of published APR. APRs cluster at 200 to 225 bps across the active set; commission rate is the primary differentiator for delegators.
+
+seo_intro: |
+ Hyperliquid runs a permissioned validator set of ~30 active nodes. Unlike
+ Solana's ~3000-validator market where yield spreads across hundreds of bps,
+ every non-jailed Hyperliquid validator earns the same gross APR from the
+ protocol; the only differentiation is commission rate, uptime, and jail
+ risk. This page surfaces the full active set, ranked by net yield
+ (gross APR after commission and downtime drag), so a delegator can see
+ exactly which operators take the smallest cut and stay online.
+
+ The data updates every 5 minutes from the Hyperliquid `/info
+ validatorSummaries` endpoint. Net yield is `predictedApr.day ×
+ uptimeFraction`, expressed in basis points (1% = 100 bps).
+
+abstract: |
+ Every 5 minutes, the validator-yield harness POSTs `{"type":
+ "validatorSummaries"}` to `api.hyperliquid.xyz/info` and reads
+ `predictedApr.day` × `stats[day].uptimeFraction` per validator. Jailed
+ validators receive `net_yield_bps = 0` regardless of their published APR.
+ Commission is taken from the `commission` field (decimal fraction, e.g.
+ `0.04` = 4% = 400 bps). Delegated stake is the `stake` field divided by
+ `1e8` (HYPE token has 8 decimal places) then multiplied by the live HYPE
+ oracle price from `metaAndAssetCtxs`. All metrics land in Prometheus under
+ `ocb_validator_net_yield_bps{chain="hyperliquid", validator=,
+ name=
+
+ )}
+
{/* Top alternatives cards - explicit list with internal links to
product pages. The bench leaderboard below carries the full data;
this section frames the answer for skim-readers and search. */}
diff --git a/src/app/api/internal/revalidate-aggregate/route.ts b/src/app/api/internal/revalidate-aggregate/route.ts
index d42da225..39d221d9 100644
--- a/src/app/api/internal/revalidate-aggregate/route.ts
+++ b/src/app/api/internal/revalidate-aggregate/route.ts
@@ -43,5 +43,6 @@ export async function POST(req: Request): Promise {
// can serve different values than the hub for up to 5 minutes after a
// worker publish.
revalidateTag("benchmarks", "default");
- return NextResponse.json({ revalidated: true, tags: ["bench-aggregate", "benchmarks"] });
+ revalidateTag("data-api-cohort", "default");
+ return NextResponse.json({ revalidated: true, tags: ["bench-aggregate", "benchmarks", "data-api-cohort"] });
}
diff --git a/src/app/api/series/[slug]/route.ts b/src/app/api/series/[slug]/route.ts
index f247e535..371358ff 100644
--- a/src/app/api/series/[slug]/route.ts
+++ b/src/app/api/series/[slug]/route.ts
@@ -183,6 +183,12 @@ export async function GET(
}
const url = new URL(req.url);
+ // When ?raw=1 the response values stay in OCB's internal unit convention
+ // (milliseconds for "s" benches, basis points for "bps" benches) so the
+ // time-series chart can pass them directly to fmtUnit without a second
+ // division. The video renderer omits raw=1 and gets the display-friendly
+ // conversion (seconds / percent) it expects.
+ const rawMode = url.searchParams.get("raw") === "1";
const rangeParam = url.searchParams.get("range") ?? "24h";
if (!isRangeKey(rangeParam)) {
return NextResponse.json(
@@ -330,16 +336,20 @@ export async function GET(
// "bps" = stored in basis points, site displays as % (÷100) → send "pct" + divide values
// "s" = stored in milliseconds, site displays as seconds → send "s" + divide values
// "pct", "bp", "usd", "count", … → pass through unchanged
+ // When ?raw=1 (time-series chart), skip conversion so fmtUnit receives
+ // values in the same internal unit as the 24h series from the bench object.
let displayUnit = sourceUnit;
const providers = rawProviders.map((p) => ({ ...p }));
- if (sourceUnit === "bps") {
- displayUnit = "pct";
- for (const p of providers) {
- p.values = p.values.map((v) => (v == null ? null : v / 100));
- }
- } else if (sourceUnit === "s") {
- for (const p of providers) {
- p.values = p.values.map((v) => (v == null ? null : v / 1000));
+ if (!rawMode) {
+ if (sourceUnit === "bps") {
+ displayUnit = "pct";
+ for (const p of providers) {
+ p.values = p.values.map((v) => (v == null ? null : v / 100));
+ }
+ } else if (sourceUnit === "s") {
+ for (const p of providers) {
+ p.values = p.values.map((v) => (v == null ? null : v / 1000));
+ }
}
}
diff --git a/src/app/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx
index fd197b5d..3f8c1bd7 100644
--- a/src/app/benchmarks/[slug]/page.tsx
+++ b/src/app/benchmarks/[slug]/page.tsx
@@ -1,6 +1,5 @@
import { Suspense } from "react";
import type { Metadata } from "next";
-import { unstable_noStore as noStore } from "next/cache";
import { notFound } from "next/navigation";
import Link from "next/link";
import { ArrowLeft, ArrowUpRight, ChevronDown } from "lucide-react";
@@ -78,7 +77,7 @@ type Params = { slug: string };
// Rendered ON DEMAND (first request, then ISR-cached). Prerendering the
// 25+ bench pages at build pushed the full multi-bench Prom load through
// the CI runner, whose DNS resolver throttles under hundreds of lookups;
-// observed 2026-06-11: /benchmarks/pm-data-freshness failing 3×60s
+// observed 2026-06-11: a bench page failing 3×60s
// export attempts and killing the deploy. Without the embedded variant
// matrix an on-demand first render is a few seconds once per deploy per
// slug, then the CDN serves it.
@@ -276,14 +275,6 @@ export default async function BenchmarkPage({
const isDraft = benchmark.status === "draft";
const isAwaiting = isDraft && benchmark.editorialStatus === "live";
- // An editorially-live bench rendering as draft means the store read
- // came back empty this cycle (srh timeout, snapshot swap), not that
- // the bench has no data: keep that render OUT of the ISR cache so the
- // next visitor triggers a fresh read instead of everyone seeing
- // "no live data yet" for a revalidate window (seen on perp-fees
- // 2026-07-10). Genuinely-new benches render dynamically until their
- // first samples land, which is the correct behavior anyway.
- if (isAwaiting) noStore();
// Insufficient: editorially live, runtime might say "live" too, but the
// shared predicate decided no provider has a usable p50. Drives the
// pill above the H1 and the headline degradation downstream.
diff --git a/src/app/benchmarks/[slug]/share-card/route.tsx b/src/app/benchmarks/[slug]/share-card/route.tsx
index 08a3a50c..38b3576d 100644
--- a/src/app/benchmarks/[slug]/share-card/route.tsx
+++ b/src/app/benchmarks/[slug]/share-card/route.tsx
@@ -209,6 +209,14 @@ function CardProviderLogo({
);
}
+ // chipBackground / chipTextColor return CSS variables ("var(--color-ink-soft)",
+ // "var(--color-paper)", "var(--color-ink)") for unbranded or dark-brand providers.
+ // Satori cannot resolve CSS variables and crashes the whole ImageResponse stream.
+ // Resolve to the local hex palette variables instead.
+ const rawBg = chipBackground(slug);
+ const chipBg = rawBg.startsWith("#") ? rawBg : INK_SOFT;
+ const rawFg = chipTextColor(slug);
+ const chipFg = rawFg === "var(--color-ink)" ? INK : PAPER;
return (
= 8 || titleLen > 55;
- const veryDense = count >= 10 || titleLen > 70;
+ // The "and N more" line counts as an extra row for density — 9 rows +
+ // truncation line overflows the 630px canvas at dense (not veryDense)
+ // sizing, pushing the text on top of the CardFooter border.
+ const effectiveCount = count + (truncatedCount > 0 ? 1 : 0);
+ const dense = effectiveCount >= 8 || titleLen > 55;
+ const veryDense = effectiveCount >= 10 || titleLen > 70;
const titleSize = veryDense ? 32 : dense ? 40 : 50;
const rankSize = veryDense ? 18 : dense ? 20 : 24;
const nameSize = veryDense ? 18 : dense ? 20 : 24;
@@ -1049,6 +1070,9 @@ async function renderSnapshot(
colors: Map,
chainLabel?: string | null
) {
+ // Cap series to avoid unreadable charts and Satori element-count limits
+ // on large benches (e.g. hyperliquid-frontends has 104 providers).
+ const MAX_SNAPSHOT_SERIES = 16;
const sorted = sortByP50(benchmark);
const seriesList = sorted
.map((r) => ({
@@ -1063,7 +1087,8 @@ async function renderSnapshot(
color: colors.get(r.slug) ?? INK_SOFT,
p50: r.ms.p50,
}))
- .filter((s) => s.values.length > 1);
+ .filter((s) => s.values.length > 1)
+ .slice(0, MAX_SNAPSHOT_SERIES);
const chartW = 1086;
const chartH = 280;
diff --git a/src/app/data-api/page.tsx b/src/app/data-api/page.tsx
new file mode 100644
index 00000000..22159832
--- /dev/null
+++ b/src/app/data-api/page.tsx
@@ -0,0 +1,306 @@
+import Link from "next/link";
+import { fetchDataApiSnapshot, GROUP_META, fmtDataValue } from "@/lib/data-api-stats";
+import { DataApiHubTabs } from "@/components/data-api-hub-tabs";
+import { pageMetadata } from "@/lib/page-metadata";
+import { safeJsonLd, buildBreadcrumbJsonLd } from "@/lib/jsonld";
+import { SITE } from "@/data/site";
+
+/**
+ * Hub landing page for the data API vertical: price feeds, token metadata,
+ * portfolio/wallet indexing, DEX coverage, and NFT data.
+ *
+ * Blob-only: loads the materialized bench blobs for each of the 9 data-API
+ * benchmarks. Never touches Prometheus at render time. Graceful degradation
+ * if blobs are missing — the page renders the "warming up" state with direct
+ * bench links, never a 404 or throw.
+ */
+
+const DESCRIPTION =
+ "Live benchmark rankings for crypto data APIs: price feed latency, token metadata coverage, wallet indexing freshness, DEX chain coverage, and NFT data quality.";
+
+export const metadata: import("next").Metadata = pageMetadata({
+ path: "/data-api",
+ title: "Best Crypto Data API 2026, ranked by benchmark",
+ description: DESCRIPTION,
+});
+
+export const revalidate = 60;
+
+const BENCH_SLUGS = [
+ "aggregator-head-lag",
+ "metadata-coverage",
+ "asset-registry-coverage",
+ "token-quote-coverage",
+ "indexing-freshness",
+ "portfolio-chain-coverage",
+ "wallet-labels-coverage",
+ "dex-network-coverage",
+ "nft-collection-metadata",
+] as const;
+
+export default async function DataApiHubPage() {
+ const snapshot = await fetchDataApiSnapshot();
+
+ const breadcrumbLd = {
+ "@context": "https://schema.org",
+ ...buildBreadcrumbJsonLd([
+ { name: "Home", item: SITE.url },
+ { name: "Data API benchmarks", item: `${SITE.url}/data-api` },
+ ]),
+ };
+
+ const itemListLd = {
+ "@context": "https://schema.org",
+ "@type": "ItemList",
+ name: "Crypto data API benchmarks by OpenChainBench",
+ description: DESCRIPTION,
+ numberOfItems: BENCH_SLUGS.length,
+ itemListElement: BENCH_SLUGS.map((slug, i) => ({
+ "@type": "ListItem",
+ position: i + 1,
+ name: slug,
+ url: `${SITE.url}/benchmarks/${slug}`,
+ })),
+ };
+
+ return (
+
+
+
+
+
+
+ Data APIs
+
+
+ Crypto data API benchmarks
+
+
+ Nine independent benchmarks across five categories: price feed head
+ lag, token metadata coverage, wallet indexing freshness, DEX network
+ coverage, and NFT data quality. Every number is measured live from
+ the same harness on the same schedule. No marketing claims, just probe
+ data.
+
+ All numbers are p50 values from the trailing 24h window unless noted.
+ Click any bench title for the full leaderboard with p90/p99, per-region
+ tabs, and time-series charts. Refresh interval 60s.
+
+ Drop these on your site to show your ranking. The SVG fetches the
+ latest figures on every request so the badge stays accurate without
+ redeploying.
+
{RANGES.map((r) => {
+ // Hide entirely once we have confirmed there's no data for this range.
+ if (r === "7d" && lazySeries7d !== null && !has7d) return null;
+ if (r === "30d" && lazySeries30d !== null && !has30d) return null;
const active = r === range;
const disabled =
(r === "7d" && !has7d) || (r === "30d" && !has30d);
@@ -537,22 +646,56 @@ export function TimeSeriesChart({
);
})}
+ {(has90d || has1y) && (
+ <>
+
+ {has90d && (
+
+ )}
+ {has1y && (
+
+ )}
+ >
+ )}
{longRangeSeries && (
<>
{/* Hairline separator between live (Prom) and archive
ranges so the reader sees they come from a different
data source — kept inside the same pill strip so the
- selection feels like one control. Panel tabs disable
- the archive ranges (panel data is not archived). */}
+ selection feels like one control. */}
{LONG_RANGES.map((r) => {
const active = r === range;
- // 90d / 1y are Prom-backed for panel views; 180d / all are
- // archive-only and stay disabled when a panel is active.
- const panelBlocks = panelActive && r !== "90d" && r !== "1y";
+ // Archive series covers all long-range windows (90d/180d/1y/all)
+ // even when a panel is active — panelBlocks no longer needed.
+ const panelBlocks = false;
const disabled = longRangeDisabled || panelBlocks;
const title = panelBlocks
? "Switch to the main metric for 180D / ALL history"
@@ -613,23 +756,25 @@ export function TimeSeriesChart({
- {lines.length === 0 ? (
-
- No time-series data emitted for this range yet.
-
- ) : (
- 0 ? reset : undefined}
- />
- )}
+
+ {lines.length === 0 ? (
+
+ No time-series data emitted for this range yet.
+
+ ) : (
+ 0 ? reset : undefined}
+ />
+ )}
+
);
}
diff --git a/src/components/time-series-chart/series.tsx b/src/components/time-series-chart/series.tsx
index afe17328..67e9f386 100644
--- a/src/components/time-series-chart/series.tsx
+++ b/src/components/time-series-chart/series.tsx
@@ -21,75 +21,99 @@ type SeriesPathsProps = {
// now speak for themselves — SeriesPaths renders a natural break wherever
// the underlying values are null.
+// Max number of end-of-line labels rendered. The legend below shows all
+// providers regardless — this cap prevents right-side label clutter when
+// dozens of lines share the same Y range.
+const MAX_INLINE_LABELS = 10;
+// Minimum vertical gap (SVG units) between consecutive placed labels so
+// name + value lines don't overlap.
+const LABEL_GAP = 27;
+
export function SeriesPaths({ drawn, unit }: SeriesPathsProps) {
+ // Limit labels to top-N non-excluded lines (drawn is pre-sorted by value).
+ const labeledSlugs = new Set(
+ drawn.filter((d) => !d.excluded).slice(0, MAX_INLINE_LABELS).map((d) => d.slug),
+ );
+
+ // Collision deflection: sort labeled lines by their natural Y position,
+ // then push each label down until it clears the previous one.
+ const labelY: Record = {};
+ const toPlace = drawn
+ .filter((d) => labeledSlugs.has(d.slug))
+ .map((d) => ({ slug: d.slug, natural: d.lastY }))
+ .sort((a, b) => a.natural - b.natural);
+ let prevBottom = -Infinity;
+ for (const item of toPlace) {
+ const y = Math.max(item.natural, prevBottom);
+ labelY[item.slug] = y;
+ prevBottom = y + LABEL_GAP;
+ }
+
return (
<>
- {drawn.map((d) => (
-
-
- {
+ const showLabel = labeledSlugs.has(d.slug);
+ const ly = labelY[d.slug] ?? d.lastY;
+ return (
+
- {/* Live pulse halo. animated outward */}
-
-
-
-
- {/* Trailing tail dot */}
-
- {/* End-of-line label */}
-
- {d.name}
-
-
- {fmtUnit(d.last, unit)}
-
-
- ))}
+
+
+ {/* Live pulse halo */}
+
+
+
+
+ {/* Trailing tail dot */}
+
+ {/* End-of-line label — only for top-N, collision-deflected */}
+ {showLabel && (
+ <>
+
+ {d.name}
+
+
+ {fmtUnit(d.last, unit)}
+
+ >
+ )}
+
+ );
+ })}
>
);
}
diff --git a/src/content/reports/rpc/2026-08-state-of-public-rpc.mdx b/src/content/reports/rpc/2026-08-state-of-public-rpc.mdx
new file mode 100644
index 00000000..342aca0b
--- /dev/null
+++ b/src/content/reports/rpc/2026-08-state-of-public-rpc.mdx
@@ -0,0 +1,200 @@
+---
+title: "Best Free Public RPC 2026: Speed, Reliability, and What to Use"
+category: "rpc"
+slug: "2026-08-state-of-public-rpc"
+publishedAt: "2026-08-01"
+period: "August 2026"
+summary: "Free public RPCs are faster than ever, and less reliable than most builders assume. This report benchmarks the leading providers by what actually matters: correctness first, latency second."
+heroFinding: "The fastest free RPC in this cohort fails one call in five. The most reliable didn't fail a single probe in the measurement window."
+author: "OpenChainBench Research"
+readingTime: 15
+canonical: "https://openchainbench.com/reports/rpc/2026-08-state-of-public-rpc"
+---
+
+
+- Free public RPCs are fast enough for most use cases, but success rates vary wildly across the cohort.
+- The fastest providers are not the most reliable. Latency rankings published by providers themselves almost always hide this.
+- As of August 2026, PublicNode is the only provider in this cohort sustaining near-100% success across five or more EVM chains simultaneously.
+- Foundation endpoints on L2s (Arbitrum, Base, Optimism) consistently trail third-party providers on latency; on reliability they match but do not lead.
+- Latency grows with geographic distance, but geographic penalties vary by region and provider — check the per-region tab for your users' location before choosing a static endpoint.
+- Across 19 chains and 3 regions (57 contests), PublicNode wins 28. dRPC, the next third-party provider, trails by 14.
+- For indexers and event listeners, filter by success rate first. A 90% success rate means hundreds of silent failures per hour at typical read rates.
+
+
+
+
+## Methodology
+
+Every number in this report is a live query against the [OpenChainBench Prometheus](https://openchainbench.com/methodology). The harness probes each provider with a rotating `eth_getBlockByNumber` request every 60 seconds from three geographic regions: `us-east-1`, `eu-west-1`, and `ap-southeast-1` (Singapore). Each probe is classified into one of five states: `ok`, `http_err`, `jsonrpc_err`, `stale` (block number behind chain tip by more than 20 blocks), or `timeout` (no response within 5 seconds).
+
+Latency figures are the p50 over a 24-hour rolling window. Success rate is the fraction of probes returning `ok`. All benchmarks are reproducible: the harness source lives at [github.com/ChainBench/OpenChainBench/harnesses](https://github.com/ChainBench/OpenChainBench/tree/main/harnesses), and the live bench pages link to the Prometheus queries that back each figure.
+
+This report covers data from the [Ethereum RPC bench](/benchmarks/ethereum-rpc), [RPC Reliability bench](/benchmarks/rpc-reliability), [RPC Capabilities bench](/benchmarks/rpc-capabilities), and a selection of per-chain RPC leaderboards. The live cross-chain view across all providers and chains is on the [RPC hub](/rpc).
+
+## The Speed Trap: Why Latency Rankings Mislead
+
+Every "fastest RPC" ranking on the web is a latency ranking. p50 milliseconds, median response time, average latency. The implicit assumption is that a faster response is a better response.
+
+That assumption breaks for free public RPCs.
+
+An RPC endpoint can return an HTTP 200 with a JSON body in milliseconds and still be wrong. If the `result` field contains a JSON-RPC error, or if the block number is 40 blocks behind the chain tip, the low latency figure is meaningless. The client asked a question and got a wrong answer, fast.
+
+OpenChainBench classifies every probe response. A 200 that contains `{"jsonrpc":"2.0","error":{"code":-32603}}` counts as `jsonrpc_err`, not as a successful call. A 200 that returns a block number 25 blocks behind the tip counts as `stale`. Only probes classified `ok` count toward the success rate.
+
+When you apply this filter, the ranking changes dramatically. Cloudflare-eth is the clearest example in this cohort: it returns sub-second HTTP 200s, but a large share carry a JSON-RPC error body (`-32046 Cannot fulfill request`) rather than a block number. Fast on latency metrics, wrong in practice. Providers that dominate raw-latency rankings often fall to the middle or bottom once correctness is factored in.
+
+Two providers are absent from the latency ranking for different reasons. Merkle sits behind an aggressive bot filter that, in our testing, locks out programmatic clients for 20 minutes after a single unauthenticated request — a benchmark of it would measure cache-hit latency, not availability. Cloudflare-eth is tracked by the harness but has no rankable latency: with nearly every probe classified as `jsonrpc_err` (`-32046 Cannot fulfill request`), there are not enough `ok` responses to calculate a meaningful p50. It remains the clearest illustration of the speed-vs-correctness split: fast HTTP round-trips, wrong answers, no latency data.
+
+
+
+The lesson is not that latency is unimportant. A 400 ms p50 would disqualify a provider for real-time frontend use regardless of its success rate. The lesson is that below 95% success rate, latency is irrelevant.
+
+### Why free public endpoints degrade
+
+Free public RPC endpoints are subsidized products. A provider offers them to acquire developer mindshare, then sells dedicated nodes, websocket connections, or archive tiers to the subset of users whose usage grows. The subsidy model creates a structural tension: the more popular the free endpoint, the more it costs to maintain it at quality, and the incentive structure suggests providers may throttle or degrade free-tier traffic as their paid base grows.
+
+The consequence shows up in success rates. A provider whose free tier is heavily used at peak hours will serve throttle errors (`429`, `-32005`) that the harness classifies as `jsonrpc_err`. Because the harness probes at a fixed 60-second cadence from three regions, it catches the degraded hour that a casual benchmark misses entirely.
+
+## Reliability Champion
+
+One provider consistently outperforms the field on success rate across multiple chains and regions: **PublicNode**.
+
+PublicNode runs near-100% success rates on Ethereum, Arbitrum, Base, Optimism, and Avalanche over the measurement window. It achieves this without a premium tier for RPC access: the project is community-funded and operates endpoints as a public good. Its architecture uses geographic PoP distribution with automatic failover. A failure in one data center is invisible to the probe.
+
+What makes PublicNode structurally different is that it has no paid conversion funnel for RPC access. Most other providers in this cohort have an incentive structure that can lead to throttling free-tier users toward paid plans. PublicNode does not. That difference shows up in the data.
+
+The practical implication is simple: if you need a free RPC for anything that fails silently on error, PublicNode is the standout option in the no-key free tier.
+
+The Ethereum cohort splits into two groups. Several providers — including PublicNode — recorded zero probe failures over the measurement window. A second group sits in the 80–82% band: dRPC at around 80% and one other provider nearby. A 80% success rate means one in five of your `eth_call` requests silently fails — 400 failed reads per hour at 100 reads per minute. Whether that is tolerable depends entirely on how your application handles errors.
+
+## Chain-by-Chain Standouts
+
+### Ethereum
+
+The Ethereum RPC market is the most competitive in the cohort. Multiple providers compete on latency within ranges that are well within acceptable frontend budgets. The differentiation on Ethereum is almost entirely on reliability, which is why the success rate column matters more than the latency column on this bench.
+
+PublicNode's Ethereum endpoint is not the fastest. It typically ranks in the middle by p50, but near the top by success rate. This inversion is a consistent pattern across the RPC dataset: the most reliable provider is rarely the fastest, because running infrastructure that stays correct under load is a different engineering problem than minimizing median response time.
+
+### L2s: Foundation Endpoints Trail on Speed
+
+On Arbitrum, Base, and Optimism, the foundation-operated endpoints (arb1.arbitrum.io, mainnet.base.org, mainnet.optimism.io) are the most obvious choice for builders new to each chain. They are the endpoints documented first in every official SDK. But they consistently trail third-party providers on latency. On reliability, foundation endpoints reach comparable success rates — they are sized for availability, not for speed under continuous load.
+
+
+
+
+
+
+
+
+
+Foundation endpoints are maintained to be accessible, not to be fast. They are sized for the long tail of infrequent callers, not for continuous probing or indexing traffic. The specialist providers that sell dedicated nodes keep their free tier competitive because it feeds developer acquisition — a different incentive from endpoints whose paid base is large enough not to care about free-tier latency.
+
+### L1s: Foundation Endpoints Dominate
+
+The pattern reverses on Solana and Monad.
+
+Solana's `api.mainnet-beta.solana.com` leads on latency among the providers in this cohort. This is structurally expected: the Solana Foundation operates the endpoint as a showcase for the chain's speed claims, and allocates engineering resources to keep it performant.
+
+
+
+Monad's `rpc.monad.xyz` returns the fastest median responses in the entire RPC dataset across all chains. Monad's parallel-execution architecture makes the on-chain layer itself much faster, and the foundation endpoint sits co-located with validator nodes. No third-party provider is competitive on raw latency at this stage.
+
+### HyperEVM / Hyperliquid
+
+Hyperliquid's EVM presents a distinct case. Hyperliquid Labs operates the primary publicly accessible RPC and the competitive pool remains small compared to EVM chains. The data below serves as a reliability monitor, not a competitive ranking — do not use latency position here to select a provider.
+
+
+
+## Regional Divergence
+
+The headline p50 in every table above is a global average. The table below shows the full latency ranking for each of the three probe regions side by side. Read across the rows: position changes between columns are real ranking shifts; value changes show how the same provider's latency grows as geographic distance increases.
+
+
+
+Latency leadership at the top is typically stable — the same provider that wins Virginia usually wins Singapore. What changes is the magnitude: every provider's p50 grows as probes travel further, and those without infrastructure in the probe's region carry the largest penalty. One pattern worth noting on the Arbitrum bench: PublicNode's Singapore p50 undercuts its Virginia figure — evidence of an Asian PoP serving L2 traffic efficiently from a closer datacenter. That is structurally rare in the free-tier cohort. No single public RPC is optimal everywhere. If your users are global, a geo-routing strategy (CDN-edge RPC selection, or multiple endpoints with latency-based fallback) beats any static choice. The [RPC Capabilities bench](/benchmarks/rpc-capabilities) has live per-region breakdowns for every provider, queryable via `?region=us-east|eu-west|sgp`.
+
+## Provider Wins Across 19 Chains
+
+19 chains, 3 probe regions — 57 regional contests. A "win" is the lowest p50 among providers returning a classified `ok` response in that region for that chain. Data snapshot: July 28, 2026 — live current rankings on the [RPC hub](/rpc).
+
+**Win count summary**
+
+| Provider | US-East | EU-West | Singapore | Total / 57 |
+|---|---:|---:|---:|---:|
+| PublicNode | 9 | 10 | 9 | 28 |
+| Foundation (own-chain) | 5 | 5 | 5 | 15 |
+| dRPC | 5 | 4 | 5 | 14 |
+
+
+Foundation endpoints win exclusively on their own chain and cannot serve traffic on other networks. Among third-party providers, PublicNode takes 28 slots across 10 chains; dRPC, the next third-party provider, trails by 14 wins (28 vs 14) concentrated on 5 chains. The per-region breakdown for every chain is live on the [RPC Capabilities bench](/benchmarks/rpc-capabilities) via `?region=us-east|eu-west|sgp`.
+
+## Reproducibility Recipe
+
+Every number in this report can be reproduced. Here is the exact curl command to run a single probe matching the harness behavior:
+
+```bash
+REQUEST_ID=$(cat /dev/urandom | LC_ALL=C tr -dc 'a-f0-9' | head -c 16)
+curl -s -X POST https://ethereum.publicnode.com \
+ -H 'Content-Type: application/json' \
+ -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBlockByNumber\",\"params\":[\"latest\",false],\"id\":\"$REQUEST_ID\"}" \
+ | jq '{ok: (.result != null), block: (.result.number | if . then tonumber else null end)}'
+```
+
+The `REQUEST_ID` rotation prevents provider-side caching of responses, which can inflate apparent performance in naive benchmarks that repeat the same request body. The harness generates a new UUID per probe for the same reason.
+
+To reproduce the archive depth test (which determines how far back `eth_getLogs` can query):
+
+```bash
+# Block 1 = genesis-era; a full-archive provider returns a result, a pruned node returns -32001
+curl -s -X POST https://ethereum.publicnode.com \
+ -H 'Content-Type: application/json' \
+ -d '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045","0x1"],"id":1}' \
+ | jq '{archive_supported: (.result != null), error_code: (.error.code // null)}'
+```
+
+A response of `null` for `result` with error code `-32001` (`missing trie node`) confirms the provider does not serve archive queries. A non-null result confirms full archive depth. The harness tests five depth levels: 300 blocks (Geth default pruning), 7,200 (recent-week), 216,000 (recent-month), 1,296,000 (recent-year), and genesis.
+
+The full harness source, including all probe types and Prometheus metric definitions, is at [harnesses/rpc-capabilities](https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities).
+
+## Decision Framework
+
+Which free RPC you should use depends on what you are building. All figures below are live on the [RPC hub](/rpc), updated every 60 seconds.
+
+
+Latency matters because it affects perceived speed for end users. Success rates above 95% are necessary but not differentiating: the user retries failed wallet operations. Use the p50 leader for each chain in your target region. For Ethereum in Asia: check the `sgp` regional filter on the [RPC Capabilities bench](/benchmarks/rpc-capabilities) for the current leader.
+
+
+
+Success rate is the primary variable. One `jsonrpc_err` on `eth_getLogs` means a missed event and a corrupted index. Use PublicNode (near-100% success, archive supported). Build in exponential backoff with a secondary fallback: even high-success providers have incident windows.
+
+
+
+Latency is critical and the public RPC market is the wrong solution. Any free public endpoint with shared infrastructure introduces queueing delay that a co-located private node eliminates. This report covers public no-key endpoints only; MEV infrastructure is outside scope.
+
+
+
+Filter first by archive depth support using the [RPC Capabilities bench](/benchmarks/rpc-capabilities), then by latency. Several Ethereum providers in this cohort do not support archive queries at all. Sending historical `eth_getLogs` to them returns an error, silently if your client does not check error codes.
+
+
+
+As of August 2026, PublicNode is the only provider in this cohort with near-100% success rate across five or more EVM chains simultaneously. If you need a single RPC provider for a multi-chain architecture, it is the only option in the no-key free tier that meets reliability thresholds across the board.
+
+
+## What to Watch Next
+
+This is the first edition of the OpenChainBench RPC Report. Starting with the next edition, each report will track month-over-month movement across three signals: success rate shifts, latency ranking changes, and incident counts per provider. Providers that degraded, chains where the competitive landscape shifted, and new entrants to the cohort will all appear in that section.
+
+The metrics most likely to move next: success rates on Base and Arbitrum foundation endpoints (both chains are actively investing in RPC infrastructure), and the provider count on HyperEVM (currently thin).
+
+If you want to be notified when the next edition publishes, follow [@OpenChainBench](https://x.com/OpenChainBench) or subscribe to the [RSS feed](/reports/rpc/rss.xml).
+
+## Sources and Reproducibility
+
+All data in this report is derived from OpenChainBench's live Prometheus instance. The underlying metrics are public and queryable.
+
+- **Live bench pages:** [ethereum-rpc](/benchmarks/ethereum-rpc), [rpc-reliability](/benchmarks/rpc-reliability), [rpc-capabilities](/benchmarks/rpc-capabilities), [arbitrum-rpc](/benchmarks/arbitrum-rpc), [base-rpc](/benchmarks/base-rpc), [optimism-rpc](/benchmarks/optimism-rpc), [solana-rpc](/benchmarks/solana-rpc), [monad-rpc](/benchmarks/monad-rpc), [hyperliquid-rpc](/benchmarks/hyperliquid-rpc)
+- **Machine-readable data:** [/api/stat/ethereum-rpc](/api/stat/ethereum-rpc), [/api/stat/rpc-reliability](/api/stat/rpc-reliability)
+- **Harness source:** [github.com/ChainBench/OpenChainBench/harnesses](https://github.com/ChainBench/OpenChainBench/tree/main/harnesses)
+- **Prometheus queries:** available via [/api/citable](/api/citable) or the methodology page
+- **License:** All data and figures in this report are published under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). You may reproduce them in editorial, research, or commercial contexts with attribution to OpenChainBench and a link to the canonical URL.
+- **Corrections:** Data errors, methodology disputes, and provider rebuttals can be filed as [GitHub issues](https://github.com/ChainBench/OpenChainBench/issues/new). Material corrections are applied in place with a dated note.
diff --git a/src/data/bench-published.json b/src/data/bench-published.json
index e4506e1e..0da710e1 100644
--- a/src/data/bench-published.json
+++ b/src/data/bench-published.json
@@ -1,40 +1,55 @@
{
"aggregator-head-lag": "2026-04-28T13:37:44.000Z",
"arbitrum-rpc": "2026-07-03T14:33:15.000Z",
+ "asset-registry-coverage": "2026-07-24T16:00:55.000Z",
"avalanche-rpc": "2026-07-03T14:33:15.000Z",
+ "axelar-gmp-latency": "2026-07-25T23:58:45.000Z",
"base-rpc": "2026-07-03T14:33:15.000Z",
"berachain-rpc": "2026-07-03T22:44:42.000Z",
"blast-rpc": "2026-07-03T22:44:42.000Z",
"bnb-rpc": "2026-07-03T14:33:15.000Z",
"bridge-fee": "2026-04-28T13:20:36.000Z",
"bridge-quote-latency": "2026-04-28T13:20:36.000Z",
- "bridge-revenue": "2026-06-02T16:07:35.000Z",
"buyback-audit": "2026-05-21T17:54:38.000Z",
"celo-rpc": "2026-07-03T22:44:42.000Z",
+ "chainlink-ccip-latency": "2026-07-25T23:25:52.000Z",
+ "cosmos-hub-rpc": "2026-07-24T12:35:13.000Z",
"cronos-rpc": "2026-07-03T22:44:42.000Z",
+ "dex-network-coverage": "2026-07-24T16:00:55.000Z",
"ethereum-rpc": "2026-07-03T14:33:15.000Z",
+ "evm-block-builders": "2026-07-16T16:01:49.000Z",
"evm-quote-latency": "2026-06-26T12:41:12.000Z",
"explorer-chain-coverage": "2026-07-07T14:11:20.000Z",
"fraxtal-rpc": "2026-07-03T22:44:42.000Z",
"gas-estimation": "2026-05-20T18:43:56.000Z",
"gnosis-rpc": "2026-07-03T22:44:42.000Z",
+ "hyperlane-message-latency": "2026-07-25T23:43:50.000Z",
"hyperliquid-frontends": "2026-06-02T16:07:35.000Z",
"hyperliquid-hip3-deployers": "2026-06-10T23:44:26.000Z",
+ "hyperliquid-rpc": "2026-07-24T02:36:11.000Z",
+ "immutable-rpc": "2026-07-26T14:46:53.000Z",
"indexing-freshness": "2026-07-07T21:42:57.000Z",
+ "injective-rpc": "2026-07-24T12:35:13.000Z",
+ "ink-rpc": "2026-07-24T12:35:13.000Z",
+ "kaia-rpc": "2026-07-24T12:35:13.000Z",
"l1-finality": "2026-05-06T19:31:46.000Z",
"l2-block-time": "2026-05-19T18:59:50.000Z",
+ "layerzero-message-latency": "2026-07-25T23:35:46.000Z",
"linea-rpc": "2026-07-03T14:33:15.000Z",
"mantle-rpc": "2026-07-03T14:33:15.000Z",
"megaeth-rpc": "2026-07-08T07:36:17.000Z",
"metadata-coverage": "2026-05-01T15:22:33.000Z",
+ "mev-protect-rpc": "2026-07-10T16:15:14.000Z",
+ "mode-rpc": "2026-07-26T14:46:53.000Z",
"monad-rpc": "2026-07-08T07:36:17.000Z",
"moonbeam-rpc": "2026-07-03T22:44:42.000Z",
- "asset-registry-coverage": "2026-05-05T14:15:05.000Z",
- "dex-network-coverage": "2026-07-24T00:00:00.000Z",
"network-fees": "2026-06-02T13:30:08.000Z",
+ "neutron-rpc": "2026-07-24T12:35:13.000Z",
"nft-collection-metadata": "2026-06-15T15:07:26.000Z",
+ "opbnb-rpc": "2026-07-24T12:35:13.000Z",
"optimism-rpc": "2026-07-03T14:33:15.000Z",
"oracle-deviation": "2026-05-21T17:54:38.000Z",
+ "osmosis-rpc": "2026-07-23T18:06:49.000Z",
"perp-execution-quality": "2026-07-03T15:11:55.000Z",
"perp-fees": "2026-05-07T14:44:23.000Z",
"perp-funding": "2026-06-11T16:01:46.000Z",
@@ -42,25 +57,39 @@
"perp-open-interest": "2026-06-24T13:14:37.000Z",
"perp-volume-share": "2026-06-24T13:14:37.000Z",
"pm-api-latency": "2026-06-16T11:48:27.000Z",
- "pm-data-freshness": "2026-06-04T17:50:08.000Z",
"pm-rate-limits": "2026-06-16T11:48:27.000Z",
+ "pm-ws-latency": "2026-07-28T21:59:34.000Z",
+ "polkadot-rpc": "2026-07-15T17:14:34.000Z",
"polygon-rpc": "2026-07-03T14:33:15.000Z",
"polymarket-resolution-delay": "2026-06-16T11:48:27.000Z",
"portfolio-chain-coverage": "2026-07-06T13:47:16.000Z",
+ "ronin-rpc": "2026-07-26T14:46:53.000Z",
"rpc-capabilities": "2026-05-20T18:43:56.000Z",
"rpc-keyed-latency": "2026-07-07T16:12:22.000Z",
+ "rpc-reliability": "2026-07-16T16:01:49.000Z",
+ "rwa-yield-accuracy": "2026-07-24T13:56:11.000Z",
"scroll-rpc": "2026-07-03T14:33:15.000Z",
- "solana-dex-quote-latency": "2026-06-02T16:07:35.000Z",
- "solana-tx-landing": "2026-05-21T12:25:08.000Z",
- "solana-tx-landing-latency": "2026-05-21T17:54:38.000Z",
+ "sei-rpc": "2026-07-26T14:46:53.000Z",
+ "solana-rpc": "2026-07-12T15:37:15.000Z",
"soneium-rpc": "2026-07-03T22:44:42.000Z",
"sonic-rpc": "2026-07-03T22:44:42.000Z",
"stablecoin-peg": "2026-05-20T18:43:56.000Z",
"stablecoin-peg-usdt-anchored": "2026-05-20T21:20:02.000Z",
"taiko-rpc": "2026-07-03T22:44:42.000Z",
"token-deployment-cost": "2026-06-09T15:34:12.000Z",
+ "token-quote-coverage": "2026-07-24T23:30:18.000Z",
+ "tokenized-stock-peg": "2026-07-14T12:09:41.000Z",
+ "tokenized-stock-weekend-drift": "2026-07-14T12:09:41.000Z",
+ "tron-rpc": "2026-07-24T02:36:11.000Z",
"unichain-rpc": "2026-07-03T22:44:42.000Z",
+ "usdy-nav-basis": "2026-07-14T16:34:18.000Z",
"validator-yield": "2026-05-21T17:54:38.000Z",
"wallet-labels-coverage": "2026-05-08T20:37:49.000Z",
+ "world-chain-rpc": "2026-07-24T12:35:13.000Z",
+ "wormhole-vaa-latency": "2026-07-24T15:13:59.000Z",
+ "ws-head-latency-base": "2026-07-16T11:03:31.000Z",
+ "ws-head-latency-ethereum": "2026-07-16T12:20:29.000Z",
+ "ws-head-latency-solana": "2026-07-16T11:03:31.000Z",
+ "xstocks-peg": "2026-07-14T16:34:18.000Z",
"zksync-rpc": "2026-07-03T22:44:42.000Z"
}
diff --git a/src/data/provider-registry.ts b/src/data/provider-registry.ts
index 9174ff0f..49e0d387 100644
--- a/src/data/provider-registry.ts
+++ b/src/data/provider-registry.ts
@@ -115,6 +115,23 @@ export const PROVIDER_REGISTRY: Record = {
"Independent crypto market data API. Token prices, OHLCV, exchange tickers, and contract/platform lookups across 300+ supported chains. Public free tier with no auth.",
twitter: "@coinpaprika",
},
+ dexpaprika: {
+ url: "https://dexpaprika.com",
+ description:
+ "DEX data API by the CoinPaprika team. REST endpoints for pools, OHLCV candles, trades, and token prices across 35+ blockchains. No auth required on the public tier.",
+ longDescription:
+ "DexPaprika is CoinPaprika's dedicated DEX pool indexer, built as a standalone product separate from the market-data API. It exposes REST endpoints for pool discovery, OHLCV candles, live trade feeds, and token prices across 35+ EVM and non-EVM chains. The public tier requires no API key, making it practical for prototyping and open-source tooling. Because DexPaprika is purpose-built for DEX data, its network list reflects actual pool-indexing depth rather than the broader chain coverage of the parent CoinPaprika market-data API.",
+ twitter: "@coinpaprika",
+ docs: "https://api.dexpaprika.com",
+ parent: "coinpaprika",
+ features: [
+ "Pool discovery and search across 35+ chains",
+ "OHLCV candles for any DEX pair",
+ "Live trade feed per pool",
+ "Token price derived from on-chain DEX data",
+ "No API key required on public tier",
+ ],
+ },
coinstats: {
url: "https://coinstats.app",
description:
@@ -1884,6 +1901,54 @@ export const PROVIDER_REGISTRY: Record = {
twitter: "@maplefinance",
docs: "https://maplefinance.gitbook.io/maple/",
},
+ "kava-official": {
+ url: "https://www.kava.io",
+ description:
+ "Kava Labs' official public EVM RPC (evm.kava.io) for the Cosmos SDK Layer 1 with native EVM execution (chain 2222). Standard EVM JSON-RPC, keyless, rate-limited per IP.",
+ twitter: "@kava_platform",
+ },
+ "zora-official": {
+ url: "https://zora.co",
+ description:
+ "Zora Network's official public RPC (rpc.zora.energy) for the OP Stack rollup dedicated to NFT minting and creative media on-chain (chain 7777777). Standard EVM JSON-RPC, keyless, rate-limited per IP.",
+ twitter: "@ourzora",
+ },
+ "abstract-official": {
+ url: "https://abs.xyz",
+ description:
+ "Abstract Foundation's official public RPC (api.mainnet.abs.xyz) for the ZK Stack validium L2 dedicated to consumer applications and NFTs (chain 2741). Standard EVM JSON-RPC, keyless, rate-limited per IP.",
+ twitter: "@abstractchain",
+ },
+ "apechain-official": {
+ url: "https://apechain.com",
+ description:
+ "ApeChain's official public RPC (rpc.apechain.com/http) for the Arbitrum Orbit L3 operated by Yuga Labs / ApeDAO, APE gas token (chain 33139). Standard EVM JSON-RPC, keyless, rate-limited per IP.",
+ twitter: "@apecoin",
+ },
+ "lisk-official": {
+ url: "https://lisk.com",
+ description:
+ "Lisk's official public RPC (rpc.api.lisk.com) for the OP Stack L2 focused on emerging-market Web3 adoption (chain 1135). Standard EVM JSON-RPC, keyless, rate-limited per IP.",
+ twitter: "@LiskHQ",
+ },
+ sentio: {
+ url: "https://app.sentio.xyz",
+ description:
+ "Sentio is a blockchain data and RPC infrastructure provider. Their public RPC node (swell-mainnet.rpc.sentio.xyz) supports Swellchain (chain 1923) without an API key.",
+ twitter: "@sentioxyz",
+ },
+ altlayer: {
+ url: "https://altlayer.io",
+ description:
+ "AltLayer is a restaked rollup infrastructure provider offering managed RPC nodes for OP Stack and Arbitrum chains. Their public endpoint (cyber.alt.technology) supports Cyber Network (chain 7560) without an API key.",
+ twitter: "@alt_layer",
+ },
+ "cyber-official": {
+ url: "https://cyber.co",
+ description:
+ "Cyber Network's official public RPC (rpc.cyber.co) for the OP Stack L2 dedicated to SocialFi and Web3 social graphs (chain 7560). Standard EVM JSON-RPC, keyless, rate-limited per IP.",
+ twitter: "@cyberconnecthq",
+ },
};
/**
diff --git a/src/lib/chains.ts b/src/lib/chains.ts
index 4fe50c29..424fb9a2 100644
--- a/src/lib/chains.ts
+++ b/src/lib/chains.ts
@@ -21,7 +21,7 @@ import {
export { CHAIN_SLUG_ALIASES, canonicalChainSlug };
-export type ChainCategory = "L1" | "L2";
+export type ChainCategory = "L1" | "L2" | "L3";
export type ChainEntry = {
slug: string;
@@ -429,6 +429,62 @@ export const CHAINS: ChainEntry[] = [
description:
"Polygon CDK zkEVM L2 (chain 13371) operated by Immutable, dedicated Web3 gaming stack, IMX gas token, 2 s block cadence, settlement onto Ethereum via zk-proofs.",
},
+ {
+ slug: "kava",
+ label: "Kava",
+ category: "L1",
+ nativeSymbol: "KAVA",
+ description:
+ "Cosmos SDK Layer 1 (chain 2222) with a native EVM execution layer, ~5 s block time under Tendermint BFT consensus, KAVA gas token on the EVM side, and IBC connectivity to the wider Cosmos ecosystem.",
+ },
+ {
+ slug: "zora",
+ label: "Zora",
+ category: "L2",
+ nativeSymbol: "ETH",
+ description:
+ "OP Stack rollup (chain 7777777) operated by Zora Network, optimised for NFT minting and creative media on-chain, ~2 s sequencer cadence, ETH gas, blob calldata settlement on Ethereum.",
+ },
+ {
+ slug: "abstract",
+ label: "Abstract",
+ category: "L2",
+ nativeSymbol: "ETH",
+ description:
+ "ZK Stack validium L2 (chain 2741) built by the Abstract Foundation, optimised for consumer applications and NFTs, ETH gas token, ~2 s block cadence, ZK proof settlement onto Ethereum.",
+ },
+ {
+ slug: "apechain",
+ label: "ApeChain",
+ category: "L3",
+ nativeSymbol: "APE",
+ description:
+ "Arbitrum Orbit L3 (chain 33139) operated by Yuga Labs / ApeDAO, dedicated to the APE ecosystem, APE gas token, ~250 ms block cadence, settlement onto Arbitrum One.",
+ },
+ {
+ slug: "lisk",
+ label: "Lisk",
+ category: "L2",
+ nativeSymbol: "ETH",
+ description:
+ "OP Stack L2 (chain 1135) operated by Onchain Foundation (formerly Lisk Foundation), focused on emerging-market Web3 adoption, ETH gas token, ~2 s block cadence, blob calldata settlement on Ethereum.",
+ },
+ {
+ slug: "swellchain",
+ label: "Swellchain",
+ category: "L2",
+ nativeSymbol: "ETH",
+ description:
+ "OP Stack L2 (chain 1923) operated by Swell Network, the restaking-native rollup built on EigenLayer, ETH gas token, ~2 s block cadence, blob calldata settlement on Ethereum.",
+ },
+ {
+ slug: "cyber",
+ label: "Cyber",
+ category: "L2",
+ nativeSymbol: "ETH",
+ description:
+ "OP Stack L2 (chain 7560) operated by Cyber, a social-layer chain for SocialFi and Web3 social graphs, ETH gas token, ~2 s block cadence, blob calldata settlement on Ethereum.",
+ },
];
export const CHAIN_BY_SLUG = new Map(CHAINS.map((c) => [c.slug, c]));
diff --git a/src/lib/compare-compute.test.ts b/src/lib/compare-compute.test.ts
new file mode 100644
index 00000000..4fee4434
--- /dev/null
+++ b/src/lib/compare-compute.test.ts
@@ -0,0 +1,158 @@
+import { describe, expect, test } from "bun:test";
+import { latestIso, fmtTs, decideWinner, parseAdHocSlug, canonicalisationTarget } from "./compare-compute";
+
+describe("latestIso", () => {
+ test("returns the most recent ISO string from a list", () => {
+ const items = [
+ { ts: "2026-01-01T00:00:00.000Z" },
+ { ts: "2026-06-15T12:00:00.000Z" },
+ { ts: "2026-03-10T00:00:00.000Z" },
+ ];
+ expect(latestIso(items, (x) => x.ts)).toBe("2026-06-15T12:00:00.000Z");
+ });
+
+ test("returns null for empty list", () => {
+ expect(latestIso([], (x: { ts: string }) => x.ts)).toBeNull();
+ });
+
+ test("skips null/undefined picks", () => {
+ const items = [
+ { ts: null as string | null },
+ { ts: "2026-05-01T00:00:00.000Z" },
+ { ts: undefined as string | undefined },
+ ];
+ expect(latestIso(items, (x) => x.ts)).toBe("2026-05-01T00:00:00.000Z");
+ });
+
+ test("returns null when all picks are null", () => {
+ const items = [{ ts: null }, { ts: null }];
+ expect(latestIso(items, (x) => x.ts)).toBeNull();
+ });
+
+ test("single item returns that item's timestamp", () => {
+ expect(
+ latestIso([{ ts: "2026-07-01T00:00:00.000Z" }], (x) => x.ts),
+ ).toBe("2026-07-01T00:00:00.000Z");
+ });
+
+ test("works with plain string array via identity pick", () => {
+ const strs = [
+ "2025-12-31T23:59:59.000Z",
+ "2026-01-01T00:00:00.000Z",
+ "2025-06-15T00:00:00.000Z",
+ ];
+ expect(latestIso(strs, (s) => s)).toBe("2026-01-01T00:00:00.000Z");
+ });
+
+ test("two equal timestamps returns either (both same)", () => {
+ const ts = "2026-07-15T10:00:00.000Z";
+ expect(latestIso([{ ts }, { ts }], (x) => x.ts)).toBe(ts);
+ });
+});
+
+describe("fmtTs", () => {
+ test("formats a valid ISO string as UTC", () => {
+ const out = fmtTs("2026-07-15T14:30:00.000Z");
+ expect(out).not.toBeNull();
+ expect(out).toContain("UTC");
+ expect(out).toContain("2026");
+ });
+
+ test("returns null for null input", () => {
+ expect(fmtTs(null)).toBeNull();
+ });
+
+ test("returns null for undefined input", () => {
+ expect(fmtTs(undefined)).toBeNull();
+ });
+
+ test("returns null for empty string", () => {
+ expect(fmtTs("")).toBeNull();
+ });
+
+ test("output replaces GMT with UTC", () => {
+ const out = fmtTs("2026-01-01T00:00:00.000Z");
+ expect(out).not.toContain("GMT");
+ expect(out).toContain("UTC");
+ });
+});
+
+describe("decideWinner", () => {
+ test("tie when p50s are equal", () => {
+ expect(decideWinner(100, 100, false)).toBe("tie");
+ expect(decideWinner(100, 100, true)).toBe("tie");
+ });
+
+ test("lower-is-better: lower p50 wins", () => {
+ expect(decideWinner(50, 100, false)).toBe("a");
+ expect(decideWinner(100, 50, false)).toBe("b");
+ });
+
+ test("higher-is-better: higher p50 wins", () => {
+ expect(decideWinner(200, 100, true)).toBe("a");
+ expect(decideWinner(100, 200, true)).toBe("b");
+ });
+
+ test("zero vs positive: zero loses in lower-is-better", () => {
+ expect(decideWinner(0, 100, false)).toBe("a");
+ });
+
+ test("zero vs positive: zero loses in higher-is-better", () => {
+ expect(decideWinner(0, 100, true)).toBe("b");
+ });
+});
+
+describe("parseAdHocSlug", () => {
+ test("parses a simple a-vs-b slug", () => {
+ expect(parseAdHocSlug("ethereum-vs-solana")).toEqual({ a: "ethereum", b: "solana" });
+ });
+
+ test("handles hyphenated provider names", () => {
+ expect(parseAdHocSlug("helius-rpc-vs-alchemy")).toEqual({
+ a: "helius-rpc",
+ b: "alchemy",
+ });
+ });
+
+ test("returns null when delimiter is absent", () => {
+ expect(parseAdHocSlug("no-delimiter-here")).toBeNull();
+ });
+
+ test("returns null when delimiter is at position 0 (empty a)", () => {
+ expect(parseAdHocSlug("-vs-something")).toBeNull();
+ });
+
+ test("returns null when b is empty after delimiter", () => {
+ expect(parseAdHocSlug("something-vs-")).toBeNull();
+ });
+
+ test("returns null when a and b are identical", () => {
+ expect(parseAdHocSlug("ethereum-vs-ethereum")).toBeNull();
+ });
+
+ test("returns null for empty string", () => {
+ expect(parseAdHocSlug("")).toBeNull();
+ });
+});
+
+describe("canonicalisationTarget", () => {
+ test("returns canonical (sorted) slug when out of order", () => {
+ expect(canonicalisationTarget("solana-vs-ethereum")).toBe("ethereum-vs-solana");
+ });
+
+ test("returns null when slug is already canonical", () => {
+ expect(canonicalisationTarget("ethereum-vs-solana")).toBeNull();
+ });
+
+ test("returns null for invalid pair slug", () => {
+ expect(canonicalisationTarget("not-a-pair")).toBeNull();
+ });
+
+ test("returns null for same-provider slug", () => {
+ expect(canonicalisationTarget("eth-vs-eth")).toBeNull();
+ });
+
+ test("alphabetical ordering determines canonical form", () => {
+ expect(canonicalisationTarget("zcash-vs-aptos")).toBe("aptos-vs-zcash");
+ });
+});
diff --git a/src/lib/data-api-stats.ts b/src/lib/data-api-stats.ts
new file mode 100644
index 00000000..86b425bd
--- /dev/null
+++ b/src/lib/data-api-stats.ts
@@ -0,0 +1,562 @@
+/**
+ * Server-side helper for the /data-api hub page. Loads bench blobs for
+ * every "data API" benchmark (price feeds, token metadata, portfolio/wallet,
+ * DEX coverage, NFT data) and assembles two views:
+ *
+ * 1. By Benchmark — groups benches by use-case, each bench has its leader,
+ * runners-up, per-region leaders (when the bench has region data), and
+ * per-chain leaders (when the bench has bestPerChain populated).
+ *
+ * 2. By Provider — cross-bench pivot: one row per unique provider, one cell
+ * per group, showing the provider's best rank + value in that group.
+ *
+ * Blob-only: reads from the CDN bench blobs published by the materialize
+ * worker. Never touches Prometheus at render time. Graceful degradation:
+ * missing/empty blobs render as null cells (never a throw).
+ */
+
+import { unstable_cache } from "next/cache";
+import { loadBenchFromBlob } from "@/lib/bench-blob";
+import type { Benchmark, ProviderResult } from "@/types/benchmark";
+
+export type DataApiGroup =
+ | "price-feeds"
+ | "token-metadata"
+ | "portfolio-wallet"
+ | "dex-coverage"
+ | "nft-data";
+
+export const GROUP_ORDER: readonly DataApiGroup[] = [
+ "price-feeds",
+ "token-metadata",
+ "portfolio-wallet",
+ "dex-coverage",
+ "nft-data",
+];
+
+export const GROUP_META: Record<
+ DataApiGroup,
+ { label: string; shortLabel: string; accent: string; description: string }
+> = {
+ "price-feeds": {
+ label: "Price & Market Data",
+ shortLabel: "Price Feeds",
+ accent: "#f59e0b",
+ description: "Real-time price feed latency from on-chain event to API emission.",
+ },
+ "token-metadata": {
+ label: "Token Metadata",
+ shortLabel: "Token Data",
+ accent: "#8b5cf6",
+ description: "Metadata coverage, asset registry depth, and quote routing capacity.",
+ },
+ "portfolio-wallet": {
+ label: "Portfolio & Wallet",
+ shortLabel: "Portfolio",
+ accent: "#10b981",
+ description: "Wallet indexing freshness, portfolio chain coverage, and label accuracy.",
+ },
+ "dex-coverage": {
+ label: "DEX Coverage",
+ shortLabel: "DEX",
+ accent: "#0ea5e9",
+ description: "Number of blockchains where each DEX indexer tracks pools and swap volumes.",
+ },
+ "nft-data": {
+ label: "NFT Data",
+ shortLabel: "NFT",
+ accent: "#f43f5e",
+ description: "Collection metadata field coverage across blue-chip NFT collections.",
+ },
+};
+
+/** Each bench's group. Adding a bench to the hub = one line here. */
+const BENCH_GROUP: Record = {
+ "aggregator-head-lag": "price-feeds",
+ "metadata-coverage": "token-metadata",
+ "asset-registry-coverage": "token-metadata",
+ "token-quote-coverage": "token-metadata",
+ "indexing-freshness": "portfolio-wallet",
+ "portfolio-chain-coverage": "portfolio-wallet",
+ "wallet-labels-coverage": "portfolio-wallet",
+ "dex-network-coverage": "dex-coverage",
+ "nft-collection-metadata": "nft-data",
+};
+
+const BENCH_SLUGS = Object.keys(BENCH_GROUP);
+
+/** Short display titles for table rows (trimmed from the full spec title). */
+const BENCH_SHORT_TITLE: Record = {
+ "aggregator-head-lag": "Price feed head lag",
+ "metadata-coverage": "Token metadata coverage",
+ "asset-registry-coverage": "Asset registry chains",
+ "token-quote-coverage": "Token quote coverage",
+ "indexing-freshness": "Wallet indexing freshness",
+ "portfolio-chain-coverage": "Portfolio chain coverage",
+ "wallet-labels-coverage": "Wallet label coverage",
+ "dex-network-coverage": "DEX network coverage",
+ "nft-collection-metadata": "NFT metadata coverage",
+};
+
+const CHAIN_LABELS: Record = {
+ base: "Base",
+ bnb: "BNB",
+ solana: "SOL",
+ robinhood: "RH",
+ ethereum: "ETH",
+ xrp: "XRP",
+ stellar: "XLM",
+ ripple: "XRP",
+ polygon: "POL",
+ avalanche: "AVAX",
+ arbitrum: "ARB",
+ optimism: "OP",
+ tron: "TRX",
+ sui: "SUI",
+ aptos: "APT",
+ ton: "TON",
+};
+
+export type RegionLeader = {
+ region: string;
+ label: string;
+ providerSlug: string;
+ providerName: string;
+ p50: number;
+};
+
+export type ChainLeader = {
+ chain: string;
+ label: string;
+ providerSlug: string;
+ providerName: string;
+ p50: number;
+};
+
+export type DataApiBenchRow = {
+ slug: string;
+ number: string;
+ title: string;
+ shortTitle: string;
+ metric: string;
+ unit: Benchmark["unit"];
+ higherIsBetter: boolean;
+ leader: { slug: string; name: string; p50: number } | null;
+ runners: { slug: string; name: string; p50: number }[];
+ providerCount: number;
+ regionLeaders: RegionLeader[];
+ chainLeaders: ChainLeader[];
+ updatedAt: string | null;
+};
+
+export type DataApiGroupRow = {
+ group: DataApiGroup;
+ benches: DataApiBenchRow[];
+};
+
+export type DataApiRegionScore = {
+ region: string;
+ label: string;
+ rank: number;
+ p50: number;
+};
+
+export type DataApiChainScore = {
+ chain: string;
+ label: string;
+ p50: number;
+};
+
+/** Per-region chain wins: in this region, the provider is #1 on these chains. */
+export type DataApiCellWin = {
+ region: string;
+ regionLabel: string;
+ chains: { chain: string; label: string }[];
+};
+
+export type DataApiProviderCell = {
+ benchSlug: string;
+ benchShortTitle: string;
+ group: DataApiGroup;
+ rank: number;
+ p50: number;
+ unit: Benchmark["unit"];
+ higherIsBetter: boolean;
+ /** Per-region ranks for benches that have extras.regions but no cellRanks. */
+ regions?: DataApiRegionScore[];
+ /** Per-region chain wins from cellRanks (supersedes regions when present). */
+ cellWins?: DataApiCellWin[];
+};
+
+export type DataApiProviderPivotRow = {
+ slug: string;
+ name: string;
+ cells: DataApiProviderCell[];
+ groups: Partial<
+ Record
+ >;
+ groupCount: number;
+};
+
+export type DataApiSnapshot = {
+ groups: DataApiGroupRow[];
+ providers: DataApiProviderPivotRow[];
+ totals: {
+ uniqueProviders: number;
+ benchCount: number;
+ groupCount: number;
+ headlinePriceFeed: { name: string; value: string } | null;
+ headlineIndexing: { name: string; value: string } | null;
+ };
+ generatedAt: string;
+};
+
+function liveResults(bench: Benchmark): ProviderResult[] {
+ return bench.results.filter(
+ (r) =>
+ r.availability !== "unavailable" &&
+ !r.unresponsive &&
+ r.ms.p50 > 0 &&
+ r.dataConfidence !== "insufficient",
+ );
+}
+
+function sortedResults(
+ results: ProviderResult[],
+ higherIsBetter: boolean,
+): ProviderResult[] {
+ return [...results].sort((a, b) =>
+ higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50,
+ );
+}
+
+function computeRegionLeaders(bench: Benchmark): RegionLeader[] {
+ const live = liveResults(bench);
+ const { regions } = bench.extras;
+ if (!regions || Object.keys(regions).length === 0) return [];
+
+ const map = new Map();
+
+ for (const provider of live) {
+ const pts = regions[provider.slug] ?? [];
+ for (const pt of pts) {
+ if (pt.region === "global") continue;
+ const normalized =
+ pt.region === "ap-southeast" ? "sgp" : pt.region;
+ const label =
+ normalized === "sgp"
+ ? "Singapore"
+ : normalized === "us-east"
+ ? "US East"
+ : "EU West";
+
+ const current = map.get(normalized);
+ const isBetter = bench.higherIsBetter
+ ? pt.p50 > (current?.p50 ?? -Infinity)
+ : pt.p50 < (current?.p50 ?? Infinity);
+
+ if (isBetter || !current) {
+ map.set(normalized, {
+ region: normalized,
+ label,
+ providerSlug: provider.slug,
+ providerName: provider.name,
+ p50: pt.p50,
+ });
+ }
+ }
+ }
+
+ return (["us-east", "eu-west", "sgp"] as const)
+ .filter((r) => map.has(r))
+ .map((r) => map.get(r)!);
+}
+
+function computeChainLeaders(bench: Benchmark): ChainLeader[] {
+ if (!bench.bestPerChain) return [];
+ return Object.entries(bench.bestPerChain)
+ .filter(([, r]) => r.availability !== "unavailable" && !r.unresponsive && r.ms.p50 > 0)
+ .map(([chain, r]) => ({
+ chain,
+ label: CHAIN_LABELS[chain] ?? chain,
+ providerSlug: r.slug,
+ providerName: r.name,
+ p50: r.ms.p50,
+ }))
+ .sort((a, b) => a.label.localeCompare(b.label));
+}
+
+const REGION_ORDER = ["us-east", "eu-west", "sgp"] as const;
+const REGION_LABELS: Record = {
+ "us-east": "US",
+ "eu-west": "EU",
+ sgp: "SGP",
+ "ap-southeast": "SGP",
+};
+
+/**
+ * For each live provider in a bench, compute their rank per probe region.
+ * Returns a map: providerSlug → sorted DataApiRegionScore[].
+ * Empty map when bench has no extras.regions data.
+ */
+function computeProviderRegionScores(
+ bench: Benchmark,
+ live: ProviderResult[],
+): Map {
+ const { regions } = bench.extras;
+ if (!regions || Object.keys(regions).length === 0) return new Map();
+
+ // Collect per-region p50 for all live providers
+ const byRegion = new Map();
+ for (const provider of live) {
+ const pts = regions[provider.slug] ?? [];
+ for (const pt of pts) {
+ if (pt.region === "global") continue;
+ const norm = pt.region === "ap-southeast" ? "sgp" : pt.region;
+ if (!byRegion.has(norm)) byRegion.set(norm, []);
+ byRegion.get(norm)!.push({ slug: provider.slug, p50: pt.p50 });
+ }
+ }
+
+ // Sort each region and assign ranks
+ const result = new Map();
+ for (const [region, entries] of byRegion.entries()) {
+ const sorted = [...entries].sort((a, b) =>
+ bench.higherIsBetter ? b.p50 - a.p50 : a.p50 - b.p50,
+ );
+ for (const [idx, { slug, p50 }] of sorted.entries()) {
+ if (!result.has(slug)) result.set(slug, []);
+ result.get(slug)!.push({
+ region,
+ label: REGION_LABELS[region] ?? region.toUpperCase(),
+ rank: idx + 1,
+ p50,
+ });
+ }
+ }
+
+ // Sort each provider's scores in canonical region order
+ for (const scores of result.values()) {
+ scores.sort(
+ (a, b) =>
+ (REGION_ORDER as readonly string[]).indexOf(a.region) -
+ (REGION_ORDER as readonly string[]).indexOf(b.region),
+ );
+ }
+
+ return result;
+}
+
+
+function toBenchRow(slug: string, bench: Benchmark): DataApiBenchRow {
+ const live = sortedResults(liveResults(bench), bench.higherIsBetter);
+ return {
+ slug,
+ number: bench.number,
+ title: bench.title,
+ shortTitle: BENCH_SHORT_TITLE[slug] ?? bench.title,
+ metric: bench.metric,
+ unit: bench.unit,
+ higherIsBetter: bench.higherIsBetter,
+ leader: live[0]
+ ? { slug: live[0].slug, name: live[0].name, p50: live[0].ms.p50 }
+ : null,
+ runners: live.slice(1, 4).map((r) => ({
+ slug: r.slug,
+ name: r.name,
+ p50: r.ms.p50,
+ })),
+ providerCount: live.length,
+ regionLeaders: computeRegionLeaders(bench),
+ chainLeaders: computeChainLeaders(bench),
+ updatedAt: bench.lastRunAt ?? null,
+ };
+}
+
+export function fmtDataValue(v: number, unit: Benchmark["unit"]): string {
+ if (!Number.isFinite(v)) return "...";
+ switch (unit) {
+ case "pct":
+ return `${v.toFixed(1)}%`;
+ case "count":
+ return v >= 1000 ? `${Math.round(v / 1000)}k` : String(Math.round(v));
+ case "s": {
+ // Convention: benches with unit "s" store ms in ms.p50 (see format.ts)
+ const s = v / 1000;
+ if (s >= 60) return `${(s / 60).toFixed(1)} min`;
+ if (s < 1) return `${Math.round(v)} ms`;
+ return `${s.toFixed(2)} s`;
+ }
+ case "sec": {
+ // True seconds (indexing-freshness, etc.)
+ if (v >= 60) return `${(v / 60).toFixed(1)} min`;
+ return `${v.toFixed(1)} s`;
+ }
+ case "ms":
+ if (v >= 1000) return `${(v / 1000).toFixed(2)} s`;
+ return `${Math.round(v)} ms`;
+ case "bps":
+ case "bp":
+ return `${v.toFixed(0)} bps`;
+ default:
+ return String(Math.round(v));
+ }
+}
+
+/**
+ * From bench.cellRanks (keys "chain|region"), extract the cells where
+ * providerSlug is rank #1, grouped by region.
+ * e.g. US → [BNB, Base], EU → [BNB], SGP → [SOL, RH]
+ */
+function computeProviderCellWins(
+ bench: Benchmark,
+ providerSlug: string,
+): DataApiCellWin[] {
+ if (!bench.cellRanks) return [];
+
+ const regionChains = new Map();
+ for (const [key, entries] of Object.entries(bench.cellRanks)) {
+ const [chain, region] = key.split("|");
+ if (chain === "all" || region === "all") continue;
+ if (entries[0]?.slug !== providerSlug) continue;
+ const chains = regionChains.get(region) ?? [];
+ chains.push(chain);
+ regionChains.set(region, chains);
+ }
+
+ if (regionChains.size === 0) return [];
+
+ return (["us-east", "eu-west", "ap-southeast", "sgp"] as const)
+ .filter((r) => regionChains.has(r))
+ .map((r) => ({
+ region: r,
+ regionLabel: REGION_LABELS[r] ?? r.toUpperCase(),
+ chains: (regionChains.get(r) ?? []).map((c) => ({
+ chain: c,
+ label: CHAIN_LABELS[c] ?? c,
+ })),
+ }));
+}
+
+async function buildSnapshot(): Promise {
+ const settled = await Promise.allSettled(
+ BENCH_SLUGS.map((s) => loadBenchFromBlob(s)),
+ );
+
+ const loaded: [string, Benchmark][] = BENCH_SLUGS.reduce<
+ [string, Benchmark][]
+ >((acc, slug, i) => {
+ const r = settled[i];
+ if (r.status === "fulfilled" && r.value) acc.push([slug, r.value]);
+ return acc;
+ }, []);
+
+ if (loaded.length === 0) return null;
+
+ // Build group rows
+ const groupBenches: Record = {
+ "price-feeds": [],
+ "token-metadata": [],
+ "portfolio-wallet": [],
+ "dex-coverage": [],
+ "nft-data": [],
+ };
+
+ for (const [slug, bench] of loaded) {
+ const group = BENCH_GROUP[slug];
+ if (!group) continue;
+ groupBenches[group].push(toBenchRow(slug, bench));
+ }
+
+ // Build provider pivot
+ type PEntry = { name: string; cells: DataApiProviderCell[] };
+ const providerMap = new Map();
+
+ for (const [slug, bench] of loaded) {
+ const group = BENCH_GROUP[slug];
+ if (!group) continue;
+ const live = sortedResults(liveResults(bench), bench.higherIsBetter);
+ const regionScores = computeProviderRegionScores(bench, live);
+ for (const [idx, r] of live.entries()) {
+ if (!providerMap.has(r.slug)) {
+ providerMap.set(r.slug, { name: r.name, cells: [] });
+ }
+ const cellWins = computeProviderCellWins(bench, r.slug);
+ const regions = cellWins.length === 0 ? regionScores.get(r.slug) : undefined;
+ providerMap.get(r.slug)!.cells.push({
+ benchSlug: slug,
+ benchShortTitle: BENCH_SHORT_TITLE[slug] ?? slug,
+ group,
+ rank: idx + 1,
+ p50: r.ms.p50,
+ unit: bench.unit,
+ higherIsBetter: bench.higherIsBetter,
+ regions: regions && regions.length > 0 ? regions : undefined,
+ cellWins: cellWins.length > 0 ? cellWins : undefined,
+ });
+ }
+ }
+
+ const providerRows: DataApiProviderPivotRow[] = [];
+ for (const [slug, { name, cells }] of providerMap.entries()) {
+ const groups: DataApiProviderPivotRow["groups"] = {};
+ for (const cell of cells) {
+ const cur = groups[cell.group];
+ if (!cur || cell.rank < cur.bestRank) {
+ groups[cell.group] = { bestRank: cell.rank, bestCell: cell };
+ }
+ }
+ providerRows.push({
+ slug,
+ name,
+ cells,
+ groups,
+ groupCount: Object.keys(groups).length,
+ });
+ }
+
+ providerRows.sort((a, b) => {
+ if (b.groupCount !== a.groupCount) return b.groupCount - a.groupCount;
+ const avgA = a.cells.reduce((s, c) => s + c.rank, 0) / (a.cells.length || 1);
+ const avgB = b.cells.reduce((s, c) => s + c.rank, 0) / (b.cells.length || 1);
+ return avgA - avgB;
+ });
+
+ // Headline stats for KPI strip
+ const headLagEntry = loaded.find(([s]) => s === "aggregator-head-lag");
+ const indexingEntry = loaded.find(([s]) => s === "indexing-freshness");
+
+ function headlineStat(
+ entry: [string, Benchmark] | undefined,
+ ): { name: string; value: string } | null {
+ if (!entry) return null;
+ const [, bench] = entry;
+ const best = sortedResults(liveResults(bench), bench.higherIsBetter)[0];
+ if (!best) return null;
+ return { name: best.name, value: fmtDataValue(best.ms.p50, bench.unit) };
+ }
+
+ const groups: DataApiGroupRow[] = GROUP_ORDER.map((g) => ({
+ group: g,
+ benches: groupBenches[g],
+ })).filter((g) => g.benches.length > 0);
+
+ return {
+ groups,
+ providers: providerRows,
+ totals: {
+ uniqueProviders: providerMap.size,
+ benchCount: loaded.length,
+ groupCount: groups.length,
+ headlinePriceFeed: headlineStat(headLagEntry),
+ headlineIndexing: headlineStat(indexingEntry),
+ },
+ generatedAt: new Date().toISOString(),
+ };
+}
+
+export const fetchDataApiSnapshot = unstable_cache(
+ buildSnapshot,
+ ["data-api-cohort"],
+ { revalidate: 60, tags: ["data-api-cohort"] },
+);
diff --git a/src/lib/downsample.test.ts b/src/lib/downsample.test.ts
new file mode 100644
index 00000000..63e6fc59
--- /dev/null
+++ b/src/lib/downsample.test.ts
@@ -0,0 +1,89 @@
+import { describe, expect, test } from "bun:test";
+import { downsample, MINI_CHART_POINTS } from "./downsample";
+
+describe("downsample", () => {
+ test("returns input as-is (filtered) when length <= target", () => {
+ expect(downsample([1, 2, 3], 10)).toEqual([1, 2, 3]);
+ expect(downsample([1, 2, 3], 3)).toEqual([1, 2, 3]);
+ });
+
+ test("filters nulls out when no downsampling needed", () => {
+ expect(downsample([1, null, 3, null, 5], 10)).toEqual([1, 3, 5]);
+ });
+
+ test("reduces to target number of points when no nulls", () => {
+ const input = Array.from({ length: 100 }, (_, i) => i + 1);
+ const out = downsample(input, 10);
+ expect(out.length).toBe(10);
+ });
+
+ test("output may be less than target when buckets are all-null", () => {
+ // 4 values → 2 buckets; second all-null → 1 output point
+ const out = downsample([10, 20, null, null], 2);
+ expect(out.length).toBeLessThan(2);
+ });
+
+ test("target=1 returns mean of all values", () => {
+ const out = downsample([1, 2, 3, 4, 5], 1);
+ expect(out).toEqual([3]);
+ });
+
+ test("target=0 returns empty array", () => {
+ expect(downsample([1, 2, 3], 0)).toEqual([]);
+ });
+
+ test("bucket mean: each output point is the mean of its bucket", () => {
+ // 4 values → 2 buckets of 2: [1,2]=1.5, [3,4]=3.5
+ const out = downsample([1, 2, 3, 4], 2);
+ expect(out).toEqual([1.5, 3.5]);
+ });
+
+ test("nulls inside a bucket are excluded from the mean", () => {
+ // 6 values, target 2 → bucketSize=3
+ // bucket 0: [10, null, 20] → mean of [10,20] = 15
+ // bucket 1: [30, null, 60] → mean of [30,60] = 45
+ const out = downsample([10, null, 20, 30, null, 60], 2);
+ expect(out[0]).toBe(15);
+ expect(out[1]).toBe(45);
+ });
+
+ test("bucket with all nulls is omitted from output", () => {
+ // 4 values → 2 buckets; second bucket all null → only 1 output
+ const out = downsample([10, 20, null, null], 2);
+ expect(out).toHaveLength(1);
+ expect(out[0]).toBe(15);
+ });
+
+ test("empty array returns empty", () => {
+ expect(downsample([], 10)).toEqual([]);
+ });
+
+ test("all-null array returns empty", () => {
+ expect(downsample([null, null, null], 10)).toEqual([]);
+ });
+
+ test("single value passthrough", () => {
+ expect(downsample([42], 5)).toEqual([42]);
+ });
+
+ test("preserves shape: monotone series stays monotone after downsampling", () => {
+ const input = Array.from({ length: 200 }, (_, i) => i * 2);
+ const out = downsample(input, 20);
+ for (let i = 1; i < out.length; i++) {
+ expect(out[i]).toBeGreaterThanOrEqual(out[i - 1]);
+ }
+ });
+
+ test("uniform series: all output points equal the same value", () => {
+ const input = Array.from({ length: 50 }, () => 7);
+ const out = downsample(input, 10);
+ expect(out.every((v) => v === 7)).toBe(true);
+ });
+});
+
+describe("MINI_CHART_POINTS constant", () => {
+ test("is a positive integer", () => {
+ expect(MINI_CHART_POINTS).toBeGreaterThan(0);
+ expect(Number.isInteger(MINI_CHART_POINTS)).toBe(true);
+ });
+});
diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts
new file mode 100644
index 00000000..9943315c
--- /dev/null
+++ b/src/lib/format.test.ts
@@ -0,0 +1,151 @@
+import { describe, expect, test } from "bun:test";
+import {
+ fmtUnit,
+ fmtValue,
+ fmtAsOfUtc,
+ unitSuffix,
+ valueInDeclaredUnit,
+} from "./format";
+
+describe("fmtUnit — ms (default latency unit)", () => {
+ test("integer ms", () => expect(fmtUnit(150, "ms")).toBe("150 ms"));
+ test("zero ms", () => expect(fmtUnit(0, "ms")).toBe("0 ms"));
+ test("sub-millisecond keeps one decimal", () => expect(fmtUnit(0.5, "ms")).toBe("0.5 ms"));
+ test("auto-flips to s at 1000ms", () => expect(fmtUnit(1000, "ms")).toBe("1.00 s"));
+ test("auto-flips to s at 5000ms", () => expect(fmtUnit(5000, "ms")).toBe("5.00 s"));
+ test("auto-flips to min at 60000ms", () => expect(fmtUnit(60000, "ms")).toBe("1.0 min"));
+ test("auto-flips to min at 120000ms", () => expect(fmtUnit(120000, "ms")).toBe("2.0 min"));
+ test("non-finite returns dash", () => expect(fmtUnit(NaN, "ms")).toBe("-"));
+ test("Infinity returns dash", () => expect(fmtUnit(Infinity, "ms")).toBe("-"));
+});
+
+describe("fmtUnit — s (latency stored as ms, displayed as seconds)", () => {
+ test("sub-10s shows one decimal", () => expect(fmtUnit(5000, "s")).toBe("5.0 s"));
+ test("0ms edge case", () => expect(fmtUnit(0, "s")).toBe("<1 s"));
+ test("exactly 60s flips to min", () => expect(fmtUnit(60_000, "s")).toBe("1.0 min"));
+ test("10s+ shows integer", () => expect(fmtUnit(15_000, "s")).toBe("15 s"));
+ test("large value in minutes", () => expect(fmtUnit(600_000, "s")).toBe("10.0 min"));
+});
+
+describe("fmtUnit — sec (true seconds gauge)", () => {
+ test("sub-minute shows seconds", () => expect(fmtUnit(45, "sec")).toBe("45.0 s"));
+ test("sub-0.1s shows <0.1 s", () => expect(fmtUnit(0.05, "sec")).toBe("<0.1 s"));
+ test("flips to min at 60s", () => expect(fmtUnit(90, "sec")).toBe("1.5 min"));
+ test("flips to h at 3600s", () => expect(fmtUnit(7200, "sec")).toBe("2.0 h"));
+ test("flips to d at 172800s", () => expect(fmtUnit(172800, "sec")).toBe("2.0 d"));
+ test("large hours no decimal", () => expect(fmtUnit(36001, "sec")).toBe("10 h"));
+});
+
+describe("fmtUnit — pct", () => {
+ test("0%", () => expect(fmtUnit(0, "pct")).toBe("0%"));
+ test("large pct one decimal", () => expect(fmtUnit(12.5, "pct")).toBe("12.5%"));
+ test("medium pct two decimals", () => expect(fmtUnit(1.5, "pct")).toBe("1.50%"));
+ test("small pct three decimals", () => expect(fmtUnit(0.033, "pct")).toBe("0.033%"));
+ test("very small pct four decimals", () => expect(fmtUnit(0.005, "pct")).toBe("0.0050%"));
+});
+
+describe("fmtUnit — bps (legacy, converts to percent)", () => {
+ test("100 bps = 1%", () => expect(fmtUnit(100, "bps")).toBe("1.00%"));
+ test("1000 bps = 10%", () => expect(fmtUnit(1000, "bps")).toBe("10.0%"));
+});
+
+describe("fmtUnit — bp (basis points as-is)", () => {
+ test("0 bps", () => expect(fmtUnit(0, "bp")).toBe("0 bps"));
+ test("near-zero shows ~0 bps", () => expect(fmtUnit(0.001, "bp")).toBe("~0 bps"));
+ test("small value two decimals", () => expect(fmtUnit(1.5, "bp")).toBe("1.50 bps"));
+ test("mid value one decimal", () => expect(fmtUnit(50.5, "bp")).toBe("50.5 bps"));
+ test("large value integer", () => expect(fmtUnit(200, "bp")).toBe("200 bps"));
+ test("negative value", () => expect(fmtUnit(-0.74, "bp")).toBe("-0.74 bps"));
+});
+
+describe("fmtUnit — slots", () => {
+ test("0 slots", () => expect(fmtUnit(0, "slots")).toBe("0 slots"));
+ test("1.0 slot (singular)", () => expect(fmtUnit(1, "slots")).toBe("1.0 slot"));
+ test("2.5 slots", () => expect(fmtUnit(2.5, "slots")).toBe("2.5 slots"));
+ test("large integer slots", () => expect(fmtUnit(100, "slots")).toBe("100 slots"));
+});
+
+describe("fmtUnit — count", () => {
+ test("zero", () => expect(fmtUnit(0, "count")).toBe("0"));
+ test("integer count", () => expect(fmtUnit(1234, "count")).toBe("1,234"));
+ test("float noise rounded", () => expect(fmtUnit(32.01, "count")).toBe("32"));
+ test("near-zero sub-0.01 shows ~0", () => expect(fmtUnit(0.001, "count")).toBe("~0"));
+ test("sub-1 shows 3 decimals", () => expect(fmtUnit(0.5, "count")).toBe("0.500"));
+ test("10k+ compacts to K", () => expect(fmtUnit(10_000, "count")).toBe("10.0K"));
+ test("1M+ compacts to M", () => expect(fmtUnit(1_500_000, "count")).toBe("1.50M"));
+ test("1B+ compacts to B", () => expect(fmtUnit(2_000_000_000, "count")).toBe("2.00B"));
+});
+
+describe("fmtUnit — usd", () => {
+ test("zero", () => expect(fmtUnit(0, "usd")).toBe("$0"));
+ test("sub-cent precision", () => expect(fmtUnit(0.005, "usd")).toBe("$0.00500"));
+ test("sub-dollar", () => expect(fmtUnit(0.5, "usd")).toBe("$0.5000"));
+ test("dollars", () => expect(fmtUnit(12.5, "usd")).toBe("$12.5"));
+ test("thousands locale formatted", () => expect(fmtUnit(1_500, "usd")).toBe("$1,500"));
+ test("large compact", () => expect(fmtUnit(2_000_000, "usd")).toBe("$2.00M"));
+ test("very small exponent", () => {
+ const out = fmtUnit(0.0000001, "usd");
+ expect(out).toMatch(/^\$.*e/);
+ });
+});
+
+describe("fmtUnit — gwei", () => {
+ test("zero", () => expect(fmtUnit(0, "gwei")).toBe("0 gwei"));
+ test("near-zero shows ~0 gwei", () => expect(fmtUnit(1e-10, "gwei")).toBe("~0 gwei"));
+ test("sub-1 three decimals", () => expect(fmtUnit(0.5, "gwei")).toBe("0.500 gwei"));
+ test("integer gwei", () => expect(fmtUnit(25, "gwei")).toBe("25 gwei"));
+ test("large compact", () => expect(fmtUnit(15_000, "gwei")).toBe("15.0K gwei"));
+});
+
+describe("valueInDeclaredUnit", () => {
+ test("unit s divides by 1000 (ms stored, s declared)", () => {
+ expect(valueInDeclaredUnit(5000, "s")).toBe(5);
+ });
+ test("other units pass through unchanged", () => {
+ expect(valueInDeclaredUnit(150, "ms")).toBe(150);
+ expect(valueInDeclaredUnit(0.5, "pct")).toBe(0.5);
+ expect(valueInDeclaredUnit(100, "usd")).toBe(100);
+ });
+});
+
+describe("fmtAsOfUtc", () => {
+ test("formats a valid ISO string", () => {
+ expect(fmtAsOfUtc("2026-07-15T14:30:00.000Z")).toBe("2026-07-15 14:30 UTC");
+ });
+ test("zero-pads month and day", () => {
+ expect(fmtAsOfUtc("2026-01-05T09:05:00.000Z")).toBe("2026-01-05 09:05 UTC");
+ });
+ test("returns null for invalid input", () => {
+ expect(fmtAsOfUtc("not-a-date")).toBeNull();
+ expect(fmtAsOfUtc("")).toBeNull();
+ });
+});
+
+describe("unitSuffix", () => {
+ test("pct suffix", () => expect(unitSuffix("pct")).toBe(" %"));
+ test("bps suffix", () => expect(unitSuffix("bps")).toBe(" %"));
+ test("bp suffix", () => expect(unitSuffix("bp")).toBe(" bps"));
+ test("slots suffix", () => expect(unitSuffix("slots")).toBe(" slots"));
+ test("count no suffix", () => expect(unitSuffix("count")).toBe(""));
+ test("usd no suffix", () => expect(unitSuffix("usd")).toBe(""));
+ test("gwei suffix", () => expect(unitSuffix("gwei")).toBe(" gwei"));
+ test("ms stays ms below 1000ms", () => expect(unitSuffix("ms", 500)).toBe(" ms"));
+ test("ms flips to s at 1000ms", () => expect(unitSuffix("ms", 1000)).toBe(" s"));
+ test("ms flips to min at 60000ms", () => expect(unitSuffix("ms", 60000)).toBe(" min"));
+ test("s unit flips to min above 60s worth of ms", () => expect(unitSuffix("s", 60_000)).toBe(" min"));
+ test("s unit stays s below 60s", () => expect(unitSuffix("s", 5_000)).toBe(" s"));
+ test("sec unit stays s below 60", () => expect(unitSuffix("sec", 30)).toBe(" s"));
+ test("sec unit flips to min at 60+", () => expect(unitSuffix("sec", 90)).toBe(" min"));
+ test("sec unit flips to h at 3600+", () => expect(unitSuffix("sec", 3600)).toBe(" h"));
+ test("sec unit flips to d at 172800+", () => expect(unitSuffix("sec", 172800)).toBe(" d"));
+ test("unknown unit defaults to ms", () => expect(unitSuffix("whatever")).toBe(" ms"));
+});
+
+describe("fmtValue (number only, no unit word)", () => {
+ test("strips trailing ms", () => expect(fmtValue(150, "ms")).toBe("150"));
+ test("strips trailing s", () => expect(fmtValue(5000, "ms")).toBe("5.00"));
+ test("strips trailing min", () => expect(fmtValue(60000, "ms")).toBe("1.0"));
+ test("keeps $ prefix for usd", () => expect(fmtValue(100, "usd")).toBe("$100"));
+ test("keeps K/M compact notation", () => expect(fmtValue(1_500_000, "count")).toBe("1.50M"));
+ test("strips % from pct", () => expect(fmtValue(5, "pct")).toBe("5.00"));
+});
diff --git a/src/lib/format.ts b/src/lib/format.ts
index 77705c6a..47d1b7dd 100644
--- a/src/lib/format.ts
+++ b/src/lib/format.ts
@@ -73,7 +73,16 @@ export function fmtUnit(value: number, unit: string) {
}
return `${value.toFixed(0)} slots`;
}
+ if (unit === "x") {
+ // Dimensionless ratio (e.g. cost-slope $1M / $1k). 3 decimal places for
+ // values near 1 (where all the differentiation lives), 2 for larger.
+ if (!Number.isFinite(value) || value === 0) return "1.000x";
+ if (value < 2) return `${value.toFixed(3)}x`;
+ if (value < 10) return `${value.toFixed(2)}x`;
+ return `${value.toFixed(1)}x`;
+ }
if (unit === "count") {
+ if (!Number.isFinite(value)) return "∞";
// Sub-unit values are common when a "count" bench is measuring an
// error or gap that converges toward zero (e.g. gas-oracle prediction
// error in gwei — PublicNode feeHistory's p50 sits around 1e-9,
@@ -117,9 +126,8 @@ export function fmtUnit(value: number, unit: string) {
// fmtUnit produced but unitSuffix independently returned " min".
if (value >= 60000) return `${(value / 60000).toFixed(1)} min`;
if (value >= 1000) return `${(value / 1000).toFixed(2)} s`;
- // Sub-millisecond values keep one decimal: pm-data-freshness's 0.5 ms
- // anchor rendered "1 ms", contradicting the 0.5 published by the
- // citable API (2x apart, flagged by the coherence audit).
+ // Sub-millisecond values keep one decimal: 0.5 ms would render as
+ // "1 ms" without this, contradicting the published citable value.
if (value > 0 && value < 1) return `${value.toFixed(1)} ms`;
return `${value.toFixed(0)} ms`;
}
@@ -165,6 +173,7 @@ export function unitSuffix(unit: string, value?: number): string {
return " ms";
}
if (unit === "slots") return " slots";
+ if (unit === "x") return "x";
if (unit === "count") return "";
if (unit === "gwei") return " gwei";
if (unit === "usd") return "";
@@ -176,6 +185,7 @@ export function unitSuffix(unit: string, value?: number): string {
export function fmtValue(value: number, unit: string): string {
// Keep K/M/B suffixes and $ prefix — they are part of the number, not a
// unit. Only strip trailing unit words that the caller renders separately.
+ if (unit === "x") return fmtUnit(value, unit).replace(/x$/, "");
return fmtUnit(value, unit).replace(/\s+(ms|s|min|h|d|bps|slots?)$/, "").replace(/\s*%$/, "");
}
diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts
index f1691a86..b12ec338 100644
--- a/src/lib/logo-manifest.ts
+++ b/src/lib/logo-manifest.ts
@@ -57,17 +57,28 @@ const RAW: Record = {
immutable: "/logos/immutable.png",
stakeme: "/logos/stakeme.jpg",
+ // RPC wave-2 chains (2026-08-02, benches 121-129)
+ kava: "/logos/kava.png",
+ zora: "/logos/zora.webp",
+ abstract: "/logos/abstract.webp",
+ apechain: "/logos/apechain.webp",
+ lisk: "/logos/lisk.webp",
+ swellchain: "/logos/swellchain.webp",
+ cyber: "/logos/cyber.webp",
+ altlayer: "/logos/altlayer.webp",
+ sentio: "/logos/sentio.png",
+
// ─── Providers ───
mobula: "/logos/mobula.svg",
codex: "/logos/codex.svg",
- predexon: "/logos/predexon.svg",
polymarket: "/logos/polymarket.png",
+ "polymarket-us": "/logos/polymarket.png",
kalshi: "/logos/kalshi.jpg",
limitless: "/logos/limitless.png",
manifold: "/logos/manifold.svg",
myriad: "/logos/myriad.png",
predictit: "/logos/predictit.svg",
- smarkets: "/logos/smarkets.svg",
+ smarkets: "/logos/smarkets.jpg",
// ─── Perp DEX cohort (perps hub /perps) ───
drift: "/logos/drift.png",
@@ -241,6 +252,7 @@ const RAW: Record = {
// ─── asset-registry-coverage + dex-network-coverage bench providers
// (bench № 005 split into 005a/005b) ───
coinpaprika: "/logos/coinpaprika.svg",
+ dexpaprika: "/logos/dexpaprika.svg",
coinstats: "/logos/coinstats.svg",
// ─── portfolio-chain-coverage bench providers (bench № 067) ───
@@ -475,6 +487,13 @@ const ALIASES: Record = {
"cronos-official": "cronos",
"fraxtal-official": "fraxtal",
"soneium-official": "soneium",
+ // RPC wave-2 chains (2026-08-02, benches 121-129)
+ "kava-official": "kava",
+ "zora-official": "zora",
+ "abstract-official": "abstract",
+ "apechain-official": "apechain",
+ "lisk-official": "lisk",
+ "cyber-official": "cyber",
};
diff --git a/src/lib/perp-stats.ts b/src/lib/perp-stats.ts
index 81cfed26..70cf55b5 100644
--- a/src/lib/perp-stats.ts
+++ b/src/lib/perp-stats.ts
@@ -314,12 +314,13 @@ async function fetchPerpCohortRaw(): Promise {
const snapshot = await readCohortSnapshot(
PERP_COHORT_KEY,
);
- // Only use the snapshot if it was written within the last 2 minutes.
- // Without this check, the 24h KV TTL means stale Prometheus data is
- // served indefinitely even though the harness publishes every minute.
+ // Fresh snapshot (written within the last 2 min) is the fast path.
if (snapshot && snapshot.ageMs < 2 * 60 * 1000) {
return snapshot.data;
}
+ // Snapshot stale or missing — try live Prom. On Vercel, promUrl()
+ // returns null (VPS Prom is not publicly exposed), so this only
+ // succeeds from the worker process.
const fresh = await fetchPerpCohortFresh();
if (fresh) {
try {
@@ -331,18 +332,41 @@ async function fetchPerpCohortRaw(): Promise {
}`,
);
}
+ return fresh;
}
- return fresh;
+ // Both paths failed. Serve the stale snapshot as a last resort so a
+ // brief worker hiccup or Vercel cache-miss race doesn't blank the
+ // page. Cap at 10 min: beyond that we surface "unavailable" so a
+ // sustained worker outage is still visible.
+ if (snapshot && snapshot.ageMs < 10 * 60 * 1000) {
+ console.warn(
+ `perp-cohort: serving stale snapshot (${Math.round(snapshot.ageMs / 1000)}s old)`,
+ );
+ return snapshot.data;
+ }
+ return null;
}
const fetchPerpCohortCached = unstable_cache(
- fetchPerpCohortRaw,
+ async () => {
+ const data = await fetchPerpCohortRaw();
+ // Throw on null so unstable_cache does not store a cached null —
+ // a single worker hiccup would otherwise blank the page for the
+ // full 60 s revalidate window. The public wrapper catches and
+ // returns null so the page still renders its fallback banner.
+ if (data === null) throw new Error("perp-cohort-unavailable");
+ return data;
+ },
["perp-cohort-v1"],
{ revalidate: 60, tags: ["perp-cohort"] },
);
export async function fetchPerpCohort(): Promise {
- return fetchPerpCohortCached();
+ try {
+ return await fetchPerpCohortCached();
+ } catch {
+ return null;
+ }
}
/**
@@ -453,18 +477,35 @@ async function fetchPerpByAssetMatrixRaw(): Promise {
}`,
);
}
+ return fresh;
+ }
+ // Prom failed — serve stale snapshot up to 10 min rather than empty.
+ if (snapshot && snapshot.ageMs < 10 * 60 * 1000 && Array.isArray(snapshot.data) && snapshot.data.length > 0) {
+ console.warn(
+ `perp-by-asset: serving stale snapshot (${Math.round(snapshot.ageMs / 1000)}s old)`,
+ );
+ return snapshot.data;
}
return fresh;
}
const fetchPerpByAssetMatrixCached = unstable_cache(
- fetchPerpByAssetMatrixRaw,
+ async () => {
+ const data = await fetchPerpByAssetMatrixRaw();
+ // Don't cache an empty result — same rationale as perp-cohort above.
+ if (data.length === 0) throw new Error("perp-by-asset-unavailable");
+ return data;
+ },
["perp-by-asset-matrix-v2"],
{ revalidate: 120, tags: ["perp-by-asset"] },
);
export async function fetchPerpByAssetMatrix(): Promise {
- return fetchPerpByAssetMatrixCached();
+ try {
+ return await fetchPerpByAssetMatrixCached();
+ } catch {
+ return [];
+ }
}
/**
diff --git a/src/lib/perp-venue-context.ts b/src/lib/perp-venue-context.ts
index e5c9d2aa..28a396b7 100644
--- a/src/lib/perp-venue-context.ts
+++ b/src/lib/perp-venue-context.ts
@@ -4,6 +4,7 @@ import {
type PerpVenueRow,
type PerpVenueType,
} from "@/lib/perp-stats";
+import type { Benchmark } from "@/types/benchmark";
import type { PerpVenueBenchRow } from "@/components/perp-venue-bench-cards";
/**
@@ -28,7 +29,7 @@ export const PERP_VENUE_META: Record<
hyperliquid: { url: "https://hyperliquid.xyz", chainLabel: "Hyperliquid L1" },
lighter: { url: "https://lighter.xyz", chainLabel: "Lighter L2" },
"gmx-v2": { url: "https://gmx.io", chainLabel: "Arbitrum", productSlug: "gmx" },
- gains: { url: "https://gains.trade", chainLabel: "Arbitrum" },
+ gains: { url: "https://gains.trade", chainLabel: "Arbitrum, Base, Polygon, ApeChain" },
dydx: { url: "https://dydx.trade", chainLabel: "Cosmos" },
drift: { url: "https://drift.trade", chainLabel: "Solana" },
vertex: { url: "https://vertexprotocol.com", chainLabel: "Arbitrum" },
@@ -72,6 +73,7 @@ export type PerpVenueContext = {
*/
export async function getPerpVenueContext(
slug: string,
+ feesAtSizeBench?: Benchmark | null,
): Promise {
// Map product slug to cohort slug for the few that differ. Today
// only GMX is split (PROVIDER_REGISTRY entry "gmx", cohort label
@@ -97,15 +99,27 @@ export async function getPerpVenueContext(
chainLabel: meta.chainLabel,
externalUrl: meta.url,
venueType: venue.venueType,
- benchRows: benchRowsForVenue(cohort, venue),
+ benchRows: benchRowsForVenue(cohort, venue, feesAtSizeBench ?? null),
};
}
export function benchRowsForVenue(
cohort: PerpCohortSummary,
venue: PerpVenueRow,
+ feesAtSizeBench?: Benchmark | null,
): PerpVenueBenchRow[] {
const cohortSize = cohort.venues.length;
+
+ const activeMarketsVenues = cohort.venues
+ .filter((v) => v.activeMarkets != null && Number.isFinite(v.activeMarkets))
+ .sort((a, b) => (b.activeMarkets as number) - (a.activeMarkets as number));
+ const activeMarketsRank =
+ activeMarketsVenues.findIndex((v) => v.slug === venue.slug) + 1 || null;
+
+ const feesAtSize = feesAtSizeBench
+ ? rankFromBench(feesAtSizeBench, venue.slug)
+ : null;
+
return [
{
benchSlug: "perp-fees",
@@ -129,6 +143,31 @@ export function benchRowsForVenue(
vsMedian: null,
tone: "cyan",
},
+ {
+ benchSlug: "perp-active-markets",
+ label: "Active markets",
+ blurb:
+ "Live count of active perp markets across 14 venues, polled every 5 minutes.",
+ rank: activeMarketsRank,
+ cohortSize: activeMarketsVenues.length || cohortSize,
+ value:
+ venue.activeMarkets != null
+ ? Math.round(venue.activeMarkets).toLocaleString("en-US")
+ : null,
+ vsMedian: null,
+ tone: "indigo",
+ },
+ {
+ benchSlug: "perp-fees-at-size",
+ label: "Fee at $100k",
+ blurb:
+ "All-in cost at $100k notional (taker + spread + impact), 24h average. Oracle venues hold flat regardless of size.",
+ rank: feesAtSize?.rank ?? null,
+ cohortSize: feesAtSize?.total ?? cohortSize,
+ value: feesAtSize?.value != null ? fmtBps(feesAtSize.value) : null,
+ vsMedian: null,
+ tone: "violet",
+ },
];
}
@@ -146,6 +185,25 @@ function rankWithinCohort(
return idx >= 0 ? idx + 1 : null;
}
+function rankFromBench(
+ bench: Benchmark,
+ slug: string,
+): { rank: number | null; total: number; value: number | null } {
+ const live = bench.results.filter(
+ (r) => r.ms?.p50 != null && Number.isFinite(r.ms.p50),
+ );
+ const sorted = [...live].sort((a, b) =>
+ bench.higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50,
+ );
+ const idx = sorted.findIndex((r) => r.slug === slug);
+ const provider = bench.results.find((r) => r.slug === slug);
+ return {
+ rank: idx >= 0 ? idx + 1 : null,
+ total: sorted.length,
+ value: provider?.ms?.p50 ?? null,
+ };
+}
+
function fmtBps(v: number | null): string | null {
if (v == null || !Number.isFinite(v)) return null;
if (Math.abs(v) >= 100) return `${v.toFixed(0)} bps`;
diff --git a/src/lib/pm-stats.ts b/src/lib/pm-stats.ts
index a5681dec..f591611c 100644
--- a/src/lib/pm-stats.ts
+++ b/src/lib/pm-stats.ts
@@ -1,16 +1,9 @@
/**
* Server-side helper for the /prediction-markets hub page. Reads the
- * `pm_venue_*` gauges exposed by the `pm-cohort-stats` harness, plus a
- * handful of cross-bench histograms already in prod:
+ * `pm_venue_*` gauges exposed by the `pm-cohort-stats` harness, plus
+ * cross-bench histograms:
* - pmapi_request_duration_seconds_bucket (bench pm-api-latency)
- * - pm_health (bench pm-data-freshness, uptime)
* - pmres_resolution_delay_seconds_bucket (bench pm-resolution-delay)
- * - pm_freshness_delta_ms_bucket (bench pm-data-freshness)
- *
- * The freshness bench publishes a histogram keyed by
- * `{provider, venue, kind}` (NOT a pre-aggregated p50/p99 gauge), so we
- * compute the quantile client-side via `histogram_quantile()` and key
- * the data-feed rows by the `provider` label.
*
* Shape mirrors `hl-builder-stats.ts` exactly so the page composes the
* same way as /hyperliquid: one server fetch, one client tab swap.
@@ -34,6 +27,9 @@ export type PmVenueRow = {
name: string;
type: PmVenueType;
chain?: string;
+ /** false = hub-only venue (volume tracked but no bench probes; no /products page) */
+ benched: boolean;
+ externalUrl?: string;
volume30d: number | null;
volume24h: number | null;
openInterest: number | null;
@@ -71,6 +67,8 @@ type VenueSeed = {
name: string;
type: PmVenueType;
chain?: string;
+ benched?: boolean;
+ externalUrl?: string;
};
type DataFeedSeed = {
@@ -81,21 +79,20 @@ type DataFeedSeed = {
};
const PM_VENUES: VenueSeed[] = [
- { slug: "polymarket", name: "Polymarket", type: "onchain", chain: "polygon" },
- { slug: "kalshi", name: "Kalshi", type: "offchain" },
- { slug: "limitless", name: "Limitless", type: "onchain", chain: "base" },
- { slug: "manifold", name: "Manifold", type: "offchain" },
- { slug: "myriad", name: "Myriad", type: "offchain" },
- { slug: "predictit", name: "PredictIt", type: "offchain" },
- { slug: "smarkets", name: "Smarkets", type: "offchain" },
+ { slug: "polymarket", name: "Polymarket", type: "onchain", chain: "polygon" },
+ { slug: "polymarket-us", name: "Polymarket US", type: "offchain", benched: false, externalUrl: "https://polymarketexchange.com" },
+ { slug: "kalshi", name: "Kalshi", type: "offchain" },
+ { slug: "limitless", name: "Limitless", type: "onchain", chain: "base" },
+ { slug: "myriad", name: "Myriad", type: "onchain", chain: "abstract" },
+ { slug: "manifold", name: "Manifold", type: "offchain" },
+ { slug: "predictit", name: "PredictIt", type: "offchain" },
+ { slug: "smarkets", name: "Smarkets", type: "offchain" },
+ { slug: "metaculus", name: "Metaculus", type: "offchain" },
];
const PM_DATA_FEEDS: DataFeedSeed[] = [
- // Mobula dropped 2026-07-19: they stopped serving the PM WebSocket
- // relay, so the hub cohort and pm-data-freshness bench no longer
- // rank a dead endpoint.
- { slug: "codex", name: "Codex", coverage: ["polymarket", "kalshi"], isReference: false },
- { slug: "predexon", name: "Predexon", coverage: ["polymarket", "kalshi", "limitless"], isReference: false },
+ // Predexon was tracked via pm-data-freshness (retired 2026-07). Re-add
+ // when a replacement freshness harness ships.
];
function promUrl(): string | null {
@@ -106,7 +103,7 @@ function promUrl(): string | null {
* worker after every tierA sweep. Bump the suffix if the summary shape
* changes so a stale-shape blob can never deserialize into a misaligned
* payload. The cohort-snapshot module appends its own `:v1`. */
-export const PM_HUB_KEY = "pm-hub";
+export const PM_HUB_KEY = "pm-hub-v2";
/**
* Fetch the venue + data feed cohort in one Promise.all fan out. Returns
@@ -139,9 +136,6 @@ export async function fetchPmCohortFresh(): Promise {
marketsAbove1m,
apiLatencyP50,
resolutionDelayP50,
- freshnessP50,
- freshnessP99,
- uptime24h,
] = await Promise.all([
queryVector(prom, `pm_venue_volume_30d_usd`),
queryVector(prom, `pm_venue_volume_24h_usd`),
@@ -163,18 +157,6 @@ export async function fetchPmCohortFresh(): Promise {
prom,
`histogram_quantile(0.5, sum by (le) (rate(pmres_resolution_delay_seconds_bucket[30d])))`,
),
- queryVector(
- prom,
- `histogram_quantile(0.5, sum by (provider, le) (rate(pm_freshness_delta_ms_bucket{venue="polymarket",provider!="polymarket"}[1h])))`,
- ),
- queryVector(
- prom,
- `histogram_quantile(0.99, sum by (provider, le) (rate(pm_freshness_delta_ms_bucket{venue="polymarket",provider!="polymarket"}[1h])))`,
- ),
- queryVector(
- prom,
- `avg_over_time(pm_health{venue="polymarket",provider!="polymarket"}[24h])`,
- ),
]);
// Map raw vectors onto the venue rows. Order of the seed list is kept
@@ -187,6 +169,8 @@ export async function fetchPmCohortFresh(): Promise {
name: v.name,
type: v.type,
chain: v.chain,
+ benched: v.benched ?? true,
+ externalUrl: v.externalUrl,
volume30d: null,
volume24h: null,
openInterest: null,
@@ -200,7 +184,7 @@ export async function fetchPmCohortFresh(): Promise {
const apply = (
series: { labels: Record; value: number }[] | null,
- field: keyof Omit,
+ field: keyof Omit,
) => {
for (const s of series ?? []) {
const v = s.labels.venue;
@@ -231,31 +215,15 @@ export async function fetchPmCohortFresh(): Promise {
row.medianResolutionDelayMin = Number.isFinite(s.value) ? s.value / 60 : null;
}
- // Data feeds. Coverage stays static (taken from the seed); the live
- // values come from the freshness bench, which keys by `provider`. The
- // T0 reference (Polymarket CLOB) is excluded from PM_DATA_FEEDS since
- // measuring it against itself would always yield zero lag.
- const freshnessP50By = new Map();
- const freshnessP99By = new Map();
- const uptimeBy = new Map();
- for (const s of freshnessP50 ?? []) {
- const r = s.labels.provider;
- if (r) freshnessP50By.set(r, s.value);
- }
- for (const s of freshnessP99 ?? []) {
- const r = s.labels.provider;
- if (r) freshnessP99By.set(r, s.value);
- }
- for (const s of uptime24h ?? []) {
- const r = s.labels.provider;
- if (r) uptimeBy.set(r, s.value);
- }
+ // Data feeds. Coverage stays static (taken from the seed). The
+ // freshness bench was retired in 2026-07, so all numeric fields are null
+ // until a replacement harness ships.
const dataFeeds: PmDataFeedRow[] = PM_DATA_FEEDS.map((f) => ({
slug: f.slug,
name: f.name,
- freshnessP50Ms: f.isReference ? null : (freshnessP50By.get(f.slug) ?? null),
- freshnessP99Ms: f.isReference ? null : (freshnessP99By.get(f.slug) ?? null),
- uptime24h: f.isReference ? null : (uptimeBy.get(f.slug) ?? null),
+ freshnessP50Ms: null,
+ freshnessP99Ms: null,
+ uptime24h: null,
coverage: f.coverage,
isReference: f.isReference,
}));
diff --git a/src/lib/pm-venue-context.ts b/src/lib/pm-venue-context.ts
index bf9c10fd..9f134760 100644
--- a/src/lib/pm-venue-context.ts
+++ b/src/lib/pm-venue-context.ts
@@ -20,22 +20,18 @@ import { logoPath } from "@/lib/logo-manifest";
export const PM_VENUE_META: Record = {
polymarket: { url: "https://polymarket.com", chainLabel: "Polygon" },
+ "polymarket-us": { url: "https://polymarketexchange.com", chainLabel: "Offchain US" },
kalshi: { url: "https://kalshi.com", chainLabel: "Offchain US" },
limitless: { url: "https://limitless.exchange", chainLabel: "Base" },
- manifold: { url: "https://manifold.markets", chainLabel: "Offchain play money" },
- myriad: { url: "https://myriad.markets", chainLabel: "Offchain multi chain" },
+ myriad: { url: "https://myriad.markets", chainLabel: "Abstract L2" },
+ manifold: { url: "https://manifold.markets", chainLabel: "Offchain" },
+ predictit: { url: "https://www.predictit.org", chainLabel: "Offchain US" },
+ smarkets: { url: "https://smarkets.com", chainLabel: "Offchain UK" },
+ metaculus: { url: "https://www.metaculus.com", chainLabel: "Offchain" },
};
-// Logo paths come from the central manifest (`src/lib/logo-manifest.ts`)
-// so a slug whose file extension changes (e.g. predexon.svg → predexon.png)
-// only needs the manifest edit, not a parallel update here. The previous
-// shape hardcoded ".png" for slugs whose actual asset was ".svg" and the
-// header in PmDataFeedSection rendered a broken-image glyph for codex,
-// mobula, and predexon.
export const PM_FEED_META: Record = {
"polymarket-clob": { url: "https://polymarket.com" },
- codex: { url: "https://codex.io" },
- predexon: { url: "https://predexon.com" },
};
export type PmVenueContext = {
@@ -118,7 +114,7 @@ export function benchRowsForVenue(
tone: "teal",
},
{
- benchSlug: "polymarket-resolution-delay",
+ benchSlug: "pm-resolution-delay",
label: "Resolution delay",
blurb: "ProposePrice anchor to QuestionResolved block, median.",
rank: rankWithinCohort(cohort.venues, "medianResolutionDelayMin", venue.slug),
@@ -128,9 +124,9 @@ export function benchRowsForVenue(
tone: "cyan",
},
{
- benchSlug: "pm-data-freshness",
- label: "Data freshness",
- blurb: "Third party relays vs the venue's canonical T0 stream.",
+ benchSlug: "pm-ws-latency",
+ label: "WS latency",
+ blurb: "Connect-to-snapshot and trade publication lag on the venue WebSocket.",
rank: null,
cohortSize,
value: null,
@@ -153,9 +149,9 @@ export function benchRowsForVenue(
export function benchRowsForDataFeed(feed: PmDataFeedRow): PmVenueBenchRow[] {
return [
{
- benchSlug: "pm-data-freshness",
- label: "Freshness vs T0",
- blurb: "Lag between this relay and Polymarket CLOB T0.",
+ benchSlug: "pm-ws-latency",
+ label: "WS latency",
+ blurb: "Venue WebSocket connect-to-snapshot and trade publication lag.",
rank: null,
cohortSize: 0,
value: feed.isReference ? null : fmtMs(feed.freshnessP50Ms),
diff --git a/src/lib/providers.ts b/src/lib/providers.ts
index 6afd3930..1d9bd289 100644
--- a/src/lib/providers.ts
+++ b/src/lib/providers.ts
@@ -490,10 +490,9 @@ async function buildProviders(): Promise {
}
// Zombie slugs from the pre-collapse pm-api-latency split. The bench
-// now ranks the parent provider only (codex, predexon), but stale
-// materialize-worker snapshots can recreate ghost /products/
-// pages with no data. Hard-block so getProvider() returns undefined and
-// the route 404s cleanly.
+// now ranks the parent provider only (codex), but stale materialize-worker
+// snapshots can recreate ghost /products/ pages with no data.
+// Hard-block so getProvider() returns undefined and the route 404s cleanly.
//
// Exported as the single source of truth. related-providers.ts imports
// this set instead of duplicating the literal — PR #728 originally
diff --git a/src/lib/ranking.test.ts b/src/lib/ranking.test.ts
new file mode 100644
index 00000000..1eaa1e25
--- /dev/null
+++ b/src/lib/ranking.test.ts
@@ -0,0 +1,114 @@
+import { describe, expect, test } from "bun:test";
+import { rankResults, leader } from "./ranking";
+import type { Benchmark, ProviderResult } from "@/types/benchmark";
+
+function r(slug: string, p50: number, p99 = p50): ProviderResult {
+ return {
+ slug,
+ name: slug,
+ ms: { p50, p90: (p50 + p99) / 2, p99, mean: p50 },
+ successRate: 100,
+ availability: "live",
+ };
+}
+
+function bench(results: ProviderResult[], higherIsBetter = false): Benchmark {
+ return {
+ slug: "test",
+ number: "001",
+ title: "Test",
+ subtitle: "",
+ lastRunAt: "",
+ status: "live",
+ editorialStatus: "live",
+ sampleSize: 0,
+ abstract: "",
+ metric: "Latency",
+ unit: "ms",
+ higherIsBetter,
+ category: "RPCs",
+ results,
+ findings: [],
+ methodology: [],
+ source: "",
+ extras: { series24h: {}, regions: {} },
+ };
+}
+
+describe("rankResults", () => {
+ test("lower-is-better sorts ascending by p50 (best first)", () => {
+ const input = [r("c", 300), r("a", 100), r("b", 200)];
+ const out = rankResults(input, false);
+ expect(out.map((x) => x.slug)).toEqual(["a", "b", "c"]);
+ });
+
+ test("higher-is-better sorts descending by p50 (best first)", () => {
+ const input = [r("c", 300), r("a", 100), r("b", 200)];
+ const out = rankResults(input, true);
+ expect(out.map((x) => x.slug)).toEqual(["c", "b", "a"]);
+ });
+
+ test("does not mutate the original array", () => {
+ const input = [r("b", 200), r("a", 100)];
+ const copy = [...input];
+ rankResults(input, false);
+ expect(input).toEqual(copy);
+ });
+
+ test("returns empty array for empty input", () => {
+ expect(rankResults([], false)).toEqual([]);
+ expect(rankResults([], true)).toEqual([]);
+ });
+
+ test("single provider returns same array contents", () => {
+ const input = [r("solo", 50)];
+ expect(rankResults(input, false)).toEqual(input);
+ });
+
+ test("tie on p50 preserves original relative order (stable sort)", () => {
+ const input = [r("a", 100), r("b", 100), r("c", 100)];
+ const out = rankResults(input, false);
+ expect(out.map((x) => x.slug)).toEqual(["a", "b", "c"]);
+ });
+
+ test("works with zero p50", () => {
+ const input = [r("a", 0), r("b", 50)];
+ expect(rankResults(input, false)[0].slug).toBe("a");
+ });
+
+ test("works with very large p50 values", () => {
+ const input = [r("slow", 999_999), r("fast", 1)];
+ expect(rankResults(input, false)[0].slug).toBe("fast");
+ expect(rankResults(input, true)[0].slug).toBe("slow");
+ });
+});
+
+describe("leader", () => {
+ test("returns the provider with the lowest p50 for lower-is-better", () => {
+ const b = bench([r("slow", 300), r("fast", 50), r("mid", 150)]);
+ expect(leader(b)?.slug).toBe("fast");
+ });
+
+ test("returns the provider with the highest p50 for higher-is-better", () => {
+ const b = bench([r("low", 10), r("high", 500), r("mid", 250)], true);
+ expect(leader(b)?.slug).toBe("high");
+ });
+
+ test("returns undefined for bench with no results", () => {
+ const b = bench([]);
+ expect(leader(b)).toBeUndefined();
+ });
+
+ test("returns the only provider when there is one", () => {
+ const b = bench([r("solo", 99)]);
+ expect(leader(b)?.slug).toBe("solo");
+ });
+
+ test("leader slug matches first entry of rankResults output", () => {
+ const results = [r("c", 300), r("a", 100), r("b", 200)];
+ const b = bench(results);
+ const top = leader(b);
+ const ranked = rankResults(results, false);
+ expect(top?.slug).toBe(ranked[0].slug);
+ });
+});
diff --git a/src/lib/removed-benches.ts b/src/lib/removed-benches.ts
index bf6c3528..f459a802 100644
--- a/src/lib/removed-benches.ts
+++ b/src/lib/removed-benches.ts
@@ -55,6 +55,9 @@ export const REMOVED_ANSWER_SLUGS = new Set([
// References solana-dex-quote-latency (staging-only, in
// REMOVED_BENCH_SLUGS). Same pattern.
"which-solana-dex-aggregator-is-the-fastest",
+ // References pm-data-freshness (bench 113, retired 2026-07). Bench
+ // removed; answer returns 404 without this guard.
+ "which-prediction-market-data-api-is-the-freshest",
]);
export const REMOVED_BENCH_SLUGS = new Set([
@@ -113,6 +116,11 @@ export const REMOVED_BENCH_SLUGS = new Set([
"rpc-keyed-latency",
"explorer-chain-coverage",
"portfolio-chain-coverage",
+ // pm-data-freshness (bench 113) retired 2026-07: Predexon (the only
+ // measured data relay) discontinued. Spec + harness removed; kept in
+ // the 410 list so any indexed URL returns Gone instead of 404.
+ // Per-venue WS freshness now lives in pm-ws-latency (bench 114).
+ "pm-data-freshness",
]);
/**
diff --git a/src/lib/reports/loader.ts b/src/lib/reports/loader.ts
new file mode 100644
index 00000000..0f10f221
--- /dev/null
+++ b/src/lib/reports/loader.ts
@@ -0,0 +1,105 @@
+import fs from "node:fs";
+import path from "node:path";
+import matter from "gray-matter";
+
+const REPORTS_DIR = path.join(process.cwd(), "src/content/reports");
+
+export type ReportMeta = {
+ title: string;
+ category: string;
+ categorySlug: string;
+ slug: string;
+ publishedAt: string;
+ period: string;
+ summary: string;
+ heroFinding: string;
+ author: string;
+ readingTime: number;
+ ogImage?: string;
+ canonical: string;
+};
+
+export type Report = ReportMeta & {
+ content: string;
+};
+
+export const REPORT_CATEGORY_META: Record<
+ string,
+ { label: string; description: string }
+> = {
+ rpc: {
+ label: "RPC",
+ description:
+ "Public no-key RPC endpoints benchmarked for latency, reliability, and archive depth across EVM chains.",
+ },
+ bridge: {
+ label: "Bridge",
+ description:
+ "Cross-chain USDC transfer routes benchmarked for quote latency and effective fee.",
+ },
+ "prediction-markets": {
+ label: "Prediction Markets",
+ description:
+ "On-chain prediction markets benchmarked for quote availability, market coverage, and resolution latency.",
+ },
+};
+
+function parseReport(filePath: string): Report {
+ const raw = fs.readFileSync(filePath, "utf8");
+ const { data, content } = matter(raw);
+ const rel = filePath.replace(REPORTS_DIR + path.sep, "");
+ const categorySlug = rel.split(path.sep)[0];
+ return {
+ title: String(data.title ?? ""),
+ category: String(data.category ?? categorySlug),
+ categorySlug,
+ slug: String(data.slug ?? ""),
+ publishedAt: String(data.publishedAt ?? ""),
+ period: String(data.period ?? ""),
+ summary: String(data.summary ?? ""),
+ heroFinding: String(data.heroFinding ?? ""),
+ author: String(data.author ?? "OpenChainBench Research"),
+ readingTime: Number(data.readingTime ?? 10),
+ ogImage: data.ogImage ? String(data.ogImage) : undefined,
+ canonical: String(data.canonical ?? ""),
+ content,
+ };
+}
+
+export function getAllReports(): Report[] {
+ if (!fs.existsSync(REPORTS_DIR)) return [];
+ const reports: Report[] = [];
+ for (const cat of fs.readdirSync(REPORTS_DIR)) {
+ const catDir = path.join(REPORTS_DIR, cat);
+ if (!fs.statSync(catDir).isDirectory()) continue;
+ for (const file of fs.readdirSync(catDir)) {
+ if (!file.endsWith(".mdx")) continue;
+ try {
+ reports.push(parseReport(path.join(catDir, file)));
+ } catch {
+ // skip malformed files
+ }
+ }
+ }
+ return reports.sort((a, b) => b.publishedAt.localeCompare(a.publishedAt));
+}
+
+export function getReportsByCategory(categorySlug: string): Report[] {
+ return getAllReports().filter((r) => r.categorySlug === categorySlug);
+}
+
+export function getReport(
+ categorySlug: string,
+ slug: string,
+): Report | undefined {
+ return getAllReports().find(
+ (r) => r.categorySlug === categorySlug && r.slug === slug,
+ );
+}
+
+export function getAllReportCategories(): string[] {
+ if (!fs.existsSync(REPORTS_DIR)) return [];
+ return fs
+ .readdirSync(REPORTS_DIR)
+ .filter((f) => fs.statSync(path.join(REPORTS_DIR, f)).isDirectory());
+}
diff --git a/src/lib/search-featured.ts b/src/lib/search-featured.ts
index 86403404..73253daf 100644
--- a/src/lib/search-featured.ts
+++ b/src/lib/search-featured.ts
@@ -19,7 +19,7 @@ import { loadSnapshotFromBlob } from "@/lib/bench-blob";
import { leader, fieldValue } from "@/lib/citation";
const FEATURED_BENCH_SLUGS = [
- "pm-data-freshness",
+ "pm-ws-latency",
"aggregator-head-lag",
"l1-finality",
"rpc-capabilities",
diff --git a/src/lib/sitemap-builder.ts b/src/lib/sitemap-builder.ts
index c700a10e..ad98bf2d 100644
--- a/src/lib/sitemap-builder.ts
+++ b/src/lib/sitemap-builder.ts
@@ -2,6 +2,7 @@ import { readFileSync, statSync } from "node:fs";
import path from "node:path";
import type { MetadataRoute } from "next";
import { getBenchmarks } from "@/data/benchmarks";
+import { getAllReports, getAllReportCategories } from "@/lib/reports/loader";
import { COMPARE_PAIRS } from "@/data/compare-pairs";
import { adHocPairs } from "@/lib/compare/adhoc-pairs";
import { REMOVED_BENCH_SLUGS } from "@/middleware";
@@ -107,6 +108,38 @@ async function safeLoad(
}
}
+function reportsRoutes(): MetadataRoute.Sitemap {
+ const entries: MetadataRoute.Sitemap = [
+ {
+ url: `${SITE.url}/reports`,
+ lastModified: BUILD_TIME,
+ changeFrequency: "monthly",
+ priority: 0.8,
+ },
+ ];
+ try {
+ for (const cat of getAllReportCategories()) {
+ entries.push({
+ url: `${SITE.url}/reports/${cat}`,
+ lastModified: BUILD_TIME,
+ changeFrequency: "monthly",
+ priority: 0.7,
+ });
+ }
+ for (const r of getAllReports()) {
+ entries.push({
+ url: `${SITE.url}/reports/${r.categorySlug}/${r.slug}`,
+ lastModified: new Date(r.publishedAt),
+ changeFrequency: "monthly",
+ priority: 0.85,
+ });
+ }
+ } catch {
+ // content dir may not exist yet
+ }
+ return entries;
+}
+
function staticHubRoutes(catalogTs: Date): MetadataRoute.Sitemap {
return [
{ url: SITE.url, lastModified: catalogTs, changeFrequency: "daily", priority: 1.0 },
@@ -494,6 +527,7 @@ async function buildFullSitemap(): Promise {
return [
...staticRoutes,
+ ...reportsRoutes(),
...benchmarkRoutes,
...providerRoutes,
...hlBuilderRoutes,
@@ -507,10 +541,13 @@ async function buildFullSitemap(): Promise {
export async function buildSitemap(): Promise {
try {
- return await buildFullSitemap();
+ const timeout = new Promise((_, reject) =>
+ setTimeout(() => reject(new Error("sitemap build timeout")), 25_000),
+ );
+ return await Promise.race([buildFullSitemap(), timeout]);
} catch (err) {
console.warn(
- "[sitemap] full build threw, returning static fallback:",
+ "[sitemap] full build threw or timed out, returning static fallback:",
err,
);
return buildStaticFallback();
diff --git a/src/lib/snapshot.ts b/src/lib/snapshot.ts
index 1bcab0d1..96a4f5fe 100644
--- a/src/lib/snapshot.ts
+++ b/src/lib/snapshot.ts
@@ -57,6 +57,7 @@ const UnitSchema = z.enum([
"slots",
"usd",
"gwei",
+ "x",
]);
const StalenessMetaSchema = z.object({
diff --git a/src/lib/spec-schema.ts b/src/lib/spec-schema.ts
index 327774c1..b2d9018f 100644
--- a/src/lib/spec-schema.ts
+++ b/src/lib/spec-schema.ts
@@ -249,7 +249,7 @@ export const SpecSchema = z
/* Metric */
metric: z.string().min(1).max(100),
/** ms / s for latencies; pct for fees as percent of notional; bps for basis points; slots for Solana slot delta. */
- unit: z.enum(["ms", "s", "sec", "pct", "bps", "bp", "count", "slots", "usd", "gwei"]),
+ unit: z.enum(["ms", "s", "sec", "pct", "bps", "bp", "count", "slots", "usd", "gwei", "x"]),
/** True when bigger numbers are better (coverage, count). Default false:
* latency, fees, drift. every existing bench is "lower is better". */
higher_is_better: z.boolean().default(false),
@@ -440,7 +440,7 @@ export const SpecSchema = z
/** The PromQL label that holds each provider's slug. Defaults to
* "builder"; other benches may use "provider", "venue", etc. */
label_key: z.string().min(1).max(40).default("builder"),
- unit: z.enum(["ms", "s", "sec", "pct", "bps", "bp", "count", "slots", "usd", "gwei"]),
+ unit: z.enum(["ms", "s", "sec", "pct", "bps", "bp", "count", "slots", "usd", "gwei", "x"]),
higher_is_better: z.boolean().default(false),
/** When false the panel is data-only: it is loaded and can feed
* ledger_columns window variants, but renders no chart tab.
diff --git a/src/lib/spec.ts b/src/lib/spec.ts
index 4aeba369..9deaf4b4 100644
--- a/src/lib/spec.ts
+++ b/src/lib/spec.ts
@@ -10,7 +10,7 @@
*/
import { cache } from "react";
-import { unstable_cache, unstable_noStore } from "next/cache";
+import { unstable_cache } from "next/cache";
import type { Benchmark } from "@/types/benchmark";
import type { Spec } from "@/lib/spec-schema";
// Imported from the isolated alias module (NOT @/lib/chains) to avoid
@@ -577,16 +577,6 @@ export const loadAllBenchmarksSafe = cache(
return await loadAllBenchmarksCached();
} catch (err) {
if (err instanceof AllBenchmarksDraftError) {
- // Opt this render out of every cache layer (ISR + CDN edge). When
- // the aggregate quorum is lost (SRH stuck, Redis blackout, worker
- // starved) the placeholder response is a degraded surrogate, NOT
- // ground truth. Without noStore the placeholder HTML would sit at
- // Vercel Edge for `revalidate` seconds and — under stale-while-
- // revalidate — for hours after backend recovery, since SWR only
- // refreshes on cache miss. Marking the response no-store forces
- // every subsequent hit to re-render, which immediately picks up
- // real data as soon as SRH is back — no cache-key bump, no deploy.
- unstable_noStore();
const specs = await loadSpecs();
return specs
.map((s) => draftPlaceholderForSpec(s))
diff --git a/src/lib/views.ts b/src/lib/views.ts
index a5566007..7d6b9356 100644
--- a/src/lib/views.ts
+++ b/src/lib/views.ts
@@ -44,6 +44,7 @@ const ALLOWED_BY_UNIT: Record = {
// benches — the leaderboard view shows the $ value per provider with
// a comparison bar, which is the at-a-glance read users want.
usd: ["countLeaderboard", "rankedBar", "donut", "distribution", "timeseries"],
+ x: ["countLeaderboard", "rankedBar", "donut", "distribution", "timeseries"],
};
/**
diff --git a/src/types/benchmark.ts b/src/types/benchmark.ts
index b542d1a7..47b3f597 100644
--- a/src/types/benchmark.ts
+++ b/src/types/benchmark.ts
@@ -139,7 +139,7 @@ export type MetricPanel = {
label: string;
description?: string;
metric: string;
- unit: "ms" | "s" | "sec" | "pct" | "bps" | "bp" | "count" | "slots" | "usd" | "gwei";
+ unit: "ms" | "s" | "sec" | "pct" | "bps" | "bp" | "count" | "slots" | "usd" | "gwei" | "x";
higherIsBetter: boolean;
/** When false the panel is data-only (feeds ledger column window
* variants) and renders no chart tab. */
@@ -233,7 +233,7 @@ export type Benchmark = {
dataConfidence?: "healthy" | "low" | "insufficient";
abstract: string;
metric: string;
- unit: "ms" | "s" | "sec" | "pct" | "bps" | "bp" | "count" | "slots" | "usd" | "gwei";
+ unit: "ms" | "s" | "sec" | "pct" | "bps" | "bp" | "count" | "slots" | "usd" | "gwei" | "x";
higherIsBetter: boolean;
/** Optional drill-down dimensions exposed by the bench. When set, the
* bench page renders one tab selector per dimension and the queries get
@@ -313,7 +313,7 @@ export type LedgerColumn = {
panel?: string;
/** Display unit override; defaults to the panel's unit (panel columns)
* or the bench unit (slot columns). */
- unit?: "ms" | "s" | "sec" | "pct" | "bps" | "bp" | "count" | "slots" | "usd" | "gwei";
+ unit?: "ms" | "s" | "sec" | "pct" | "bps" | "bp" | "count" | "slots" | "usd" | "gwei" | "x";
/** Per-window value sources for the ledger's timeframe toggle: window
* key to metric_panels id. Columns without a mapping keep their 24h
* value when a longer window is selected. */