From edbcbb249d5f41f458788f4297ac4798f2f5cc3f Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:41:12 +0300 Subject: [PATCH 01/60] feat(staging): add 4 production-ready benchmarks to dev only (PR F) (#740) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After dev → main reconciliation (PRs A-E), dev was hard-reset to main. This commit re-introduces the 4 production-ready benchmarks that lived on the old dev branch as staging-only content. They will NOT be promoted to main until they pass staging review. Benches: - bridge-revenue.yml (188L, status: live) — bridge revenue tracking - evm-quote-latency.yml (194L, status: live) — EVM aggregator quote latency (Mobula vs Codex vs GeckoTerminal etc.) - solana-dex-quote-latency.yml (192L, status: live) — Solana DEX quote latency (Jupiter vs Mobula vs others) - solana-tx-landing.yml (218L, status: live) — Solana tx landing benchmark Companion editorial content (for SEO long-tail): - answers/which-evm-aggregator-has-the-fastest-quote.yml - answers/which-solana-dex-aggregator-is-the-fastest.yml - alternatives/jupiter.yml Workflow going forward: - main = prod (openchainbench.com), source of truth - dev = staging buffer (auto-deploy to staging Preview URL) - These 4 benches live on dev. When approved, open a dedicated PR cherry-picking them to main. - Future feature work: open a feature branch from dev (or main), PR to dev for staging review, then promote to main. Co-authored-by: Florent Tapponnier --- alternatives/jupiter.yml | 13 ++ ...h-evm-aggregator-has-the-fastest-quote.yml | 42 ++++ ...h-solana-dex-aggregator-is-the-fastest.yml | 42 ++++ benchmarks/bridge-revenue.yml | 188 +++++++++++++++ benchmarks/evm-quote-latency.yml | 194 ++++++++++++++++ benchmarks/solana-dex-quote-latency.yml | 192 +++++++++++++++ benchmarks/solana-tx-landing.yml | 218 ++++++++++++++++++ 7 files changed, 889 insertions(+) create mode 100644 alternatives/jupiter.yml create mode 100644 answers/which-evm-aggregator-has-the-fastest-quote.yml create mode 100644 answers/which-solana-dex-aggregator-is-the-fastest.yml create mode 100644 benchmarks/bridge-revenue.yml create mode 100644 benchmarks/evm-quote-latency.yml create mode 100644 benchmarks/solana-dex-quote-latency.yml create mode 100644 benchmarks/solana-tx-landing.yml diff --git a/alternatives/jupiter.yml b/alternatives/jupiter.yml new file mode 100644 index 000000000..d9822c022 --- /dev/null +++ b/alternatives/jupiter.yml @@ -0,0 +1,13 @@ +slug: jupiter +target_product: Jupiter +target_url: https://jup.ag +description: Solana DEX aggregator with multi-venue routing +benchmark: solana-dex-quote-latency + +intro: | + Jupiter is a Solana DEX aggregator that routes swaps across most major venues on the network, exposing a public quote endpoint at `lite-api.jup.ag/swap/v1/quote`. Quote latency sits on the hot path of every swap a wallet, trading UI or routing front-end builds: a 600 ms response pushes a perceptible delay onto the user, while an 80 ms response feels instant. This benchmark measures the wall-clock round-trip latency of Solana DEX aggregator quote APIs every 60 seconds from three Railway regions (us-east, eu-west, sgp), against tokens that are actually trending on Solana rather than the canonical SOL to USDC pair that every provider's edge cache already memorises. The harness maintains a 30-minute sliding window of bonded tokens from Mobula Pulse V2's WebSocket feed (post-bonding-curve graduates from Pump.fun, Meteora, Raydium LaunchLab and friends), picks one at random each tick and sends an identical 100 USDC to tokenOut quote request at 1 percent slippage to each provider in parallel. The HTTP client reuses TCP and TLS connections across ticks so the recorded latency reflects steady-state cost, not a cold-start handshake. Liquidity-gap failures (a provider that cannot route the picked token) land on a separate `solana_quote_no_route_total` counter and are excluded from the latency histogram, so a provider that fails fast on long-tail coverage is not penalised on the percentiles. Histogram buckets cover 10 ms to 5 s. Providers measured alongside Jupiter are Mobula, OpenOcean and Raydium; per-region tabs at the top of the page surface where each provider's infrastructure actually sits. + +seo_title: Jupiter alternatives. Live Solana DEX quote latency benchmark +seo_description: Compare Jupiter against Mobula, OpenOcean and Raydium on live Solana quote latency. 100 USDC to long-tail token, polled every 60 seconds from 3 regions. + +status: live diff --git a/answers/which-evm-aggregator-has-the-fastest-quote.yml b/answers/which-evm-aggregator-has-the-fastest-quote.yml new file mode 100644 index 000000000..b1d85c8b0 --- /dev/null +++ b/answers/which-evm-aggregator-has-the-fastest-quote.yml @@ -0,0 +1,42 @@ +slug: which-evm-aggregator-has-the-fastest-quote +question: "Which EVM swap quote API is the fastest in 2026?" +short_answer: | + {{best_name}} currently returns EVM swap quotes the fastest at {{best_p50}} (p50, 24h) across Mobula, KyberSwap, Bebop, CoW, Enso, LI.FI and OpenOcean on Ethereum, Base, Arbitrum and BSC, measured live by OpenChainBench from 3 regions. + +benchmark: evm-quote-latency + +intro: | + EVM swap aggregator APIs all promise sub second quotes. This page measures how often they actually deliver one. Every wallet integration, intent layer and routing front end is bound by the quote API it sits behind, and the gap between a 200 ms and a 1500 ms p99 is the difference between a swap UI that feels native and one that needs a loading spinner on every input change. OpenChainBench rotates a 5 pair basket across Ethereum, Base, Arbitrum and BSC (1 ETH to USDT on Ethereum, 1000 USDC to USDT on Ethereum, 0.5 ETH to USDC on Base, 100 USDC to ETH on Arbitrum, 1 BNB to USDT on BSC), fires a quote request at every supported provider in parallel every 60 seconds, and records the wall clock round trip. The leaderboard sorts by p50 latency over the last 24 hours. A geographic split is available with the same probe running independently from us-east, eu-west and Singapore, so an integrator can pick the provider whose edge gateway is closest to their backend. + +methodology: | + Each provider's public quote endpoint is hit on the 5 fixed pairs above, rotating one pair per 60 second tick. Per (provider, chain) the bench lands roughly 288 samples in 24 hours per region, enough to put p90 and p99 buckets on a solid base. Latency is wall clock from request dispatch to last byte received, observed only on the happy path (HTTP 2xx with a parseable output amount). Failures land on dedicated counters by class (auth via 401/403, throttle via 429, no route via empty or zero output amount on 200, other for everything else) and pull the success gauge to 0. Native asset sentinel is the canonical `0xEeeeeEeee` form; Odos uses the all zero form and Bebop substitutes the wrapped equivalent on the sell side, the adapter normalises per provider. The three regions are separate Railway services writing the same metric family with a `region` label, so the page can filter to a single edge or aggregate across all three. + +limitations: + - "Speed only, quote quality is not scored. Two providers can return the same time to first byte while one quotes a noticeably worse price. Converting all outputs to a comparable USD value introduces an oracle bias the leaderboard does not want to bake into a latency ranking. The fee benchmark measures realised cost separately." + - "Each provider declares its supported chain set. CoW Protocol skips BSC because CoW Settlement does not deploy there; the harness skips probes for chains a provider does not claim to cover so the failure counter is not inflated by structural non coverage." + - "Enso uses a global anonymous bucket at roughly 1 request per second, so about 20% of probes hit 429 and are dropped. Remaining samples keep p50 and p90 stable but the success rate is capped near 80%, which the leaderboard surfaces honestly." + - "The 5 pair basket favours liquid USDC and ETH routes. A long tail token route (low cap memecoin, RWA, niche bridge wrapper) has different route search complexity and the latency curves on this leaderboard do not generalise to those cases." + - "Polygon and Optimism are slated for a v2 expansion once they reach a comparable sample volume; the current Ethereum, Base, Arbitrum and BSC coverage is the integration grade core." + +faq: + - q: "Which EVM swap quote API has the lowest latency right now?" + a: "{{best_name}} currently leads at {{best_p50}} (p50 over the last 24 hours) on the active tab, across {{count}} measured providers. The leaderboard re-sorts every minute on fresh Prometheus samples, so the answer reflects measured latency on the live basket, not a marketing claim. Per chain leaders can differ from the cross chain aggregate; the chain tabs at the top of the bench page show each ranking separately." + - q: "Does this measure quote quality or just speed?" + a: "Speed only. Latency is wall clock round trip from request to last byte, recorded only on a successful parseable response. Output amount in USD is not scored on this bench because providers measure it differently (Bebop is net of fees on a gasless RFQ, others are gross), and converting all outputs to a comparable USD value introduces an oracle bias the leaderboard does not want to bake into a latency comparison." + - q: "Why are some providers not on every chain?" + a: "Each provider declares its supported chain set. The harness skips probes for chains a provider does not claim to cover so the failure counter is not inflated by structural non coverage. CoW skips BSC because CoW Settlement does not deploy there; LI.FI and OpenOcean cover 30+ chains each. The per chain tab on the bench page shows which providers actually run on each chain." + - q: "What happens when a provider rate limits the harness?" + a: "Each adapter classifies the response. 429 lands on a throttle counter and the success gauge drops to 0. 401 and 403 land on an auth counter. Empty or zero output amounts on a 200 land on a no route counter. None of those failure modes contribute to the latency histogram, so the p50 number stays representative of healthy quotes." + - q: "How is the geographic split implemented?" + a: "Three identical Railway services run the harness with different `MONITOR_REGION` env vars (us-east, eu-west, sgp). Each writes the same metric family with a `region` label. The bench page region filter selects which slice of the histogram is queried; the per chain x per region grid shows a 4 chain by 3 region cell ranking for each provider." + - q: "Why does Bebop sometimes win on latency despite being RFQ?" + a: "Bebop's RFQ model resolves price through a maker network rather than via on chain simulation. The output amount it quotes is already net of fees because settlement is gasless for the taker. The latency the bench measures is the maker network resolving the price, not a public mempool route search; on liquid pairs that path can clock comparably to or below pre warmed aggregator graphs." + +related: + - which-solana-dex-aggregator-is-the-fastest + - which-bridge-has-the-fastest-quote-api + - which-crypto-price-api-is-the-fastest + +seo_title: "Which EVM swap quote API is the fastest in 2026?" +seo_description: "{{best_name}} leads EVM swap quote latency at {{best_p50}} (p50, 24h) across Mobula, KyberSwap, Bebop, CoW, Enso, LI.FI and OpenOcean on Ethereum, Base, Arbitrum and BSC." +status: live diff --git a/answers/which-solana-dex-aggregator-is-the-fastest.yml b/answers/which-solana-dex-aggregator-is-the-fastest.yml new file mode 100644 index 000000000..11c45532c --- /dev/null +++ b/answers/which-solana-dex-aggregator-is-the-fastest.yml @@ -0,0 +1,42 @@ +slug: which-solana-dex-aggregator-is-the-fastest +question: "Which Solana DEX aggregator returns the fastest swap quote?" +short_answer: | + {{best_name}} currently returns Solana swap quotes the fastest at {{best_p50}} (p50, 24h) across Jupiter, Mobula, OpenOcean and Raydium on a rotating trending token basket, measured live by OpenChainBench from 3 regions. + +benchmark: solana-dex-quote-latency + +intro: | + Solana DEX aggregators sit on the hot path of every swap a wallet, trading UI or routing front end builds. A quote API at 80 ms feels instant to the user; a quote API at 600 ms pushes a perceptible delay between input change and updated output. Most "fastest aggregator" comparisons hardcode the canonical SOL to USDC pair, which Jupiter's lite endpoint serves from a CloudFront edge in 30 to 50 ms, measuring cache hit rather than routing. This page answers the question integrators actually need answered. Which Solana DEX aggregator returns the fastest quote when the target token is one nobody has cached, against the long tail tokens actually trending right now. OpenChainBench rotates the target every tick by picking a fresh bonded token from Mobula Pulse V2's live WebSocket feed (post bonding curve graduates from Pump.fun, Meteora, Raydium LaunchLab and friends), then fires a 100 USDC to tokenOut quote at 1% slippage against each provider in parallel from us-east, eu-west and Singapore. + +methodology: | + Every 60 seconds, in each of three Railway regions, the harness picks one Solana token from a sliding 30 minute window of bonded tokens emitted by Mobula Pulse V2 over WebSocket, then sends an identical 100 USDC to tokenOut quote (1% slippage, no fee, no referrer) to each provider. Latency is wall clock from HTTP request dispatch to the first byte of a response body containing a usable quoted output amount (Jupiter `outAmount`, Mobula `data.amountOutTokens`, OpenOcean `data.outAmount`, Raydium `data.outputAmount`). The HTTP client reuses TCP and TLS connections across ticks, so the recorded number is the steady state round trip a long lived backend integration sees, not the one off cold start handshake. Histogram buckets are 10, 25, 50, 100, 200, 500, 1000, 2000 and 5000 ms; p50, p90 and p99 are computed per region via Prometheus `histogram_quantile` over 24h. Failure classes (HTTP 429 throttled, 401/403 auth error, no route, other) each land on their own counter and are excluded from the latency histogram. + +limitations: + - "Raydium's compute API is single venue (Raydium AMM v4, CPMM, CLMM only) and does not multi hop via SOL like Jupiter or Mobula. On the Pulse fed rotation (mostly Pump.fun graduates living on PumpSwap, plus Meteora and Orca pools) Raydium returns INSUFFICIENT_LIQUIDITY on roughly 80% of picks. Its latency cells go blank below 50 successful quotes per 24h; the success rate column is its honest score." + - "Jupiter serves quotes from regional pods behind CloudFront (`x-region: eu-central-1` observed from EU, `x-pod-name: jupiter-core-*`), so each probe region reaches a nearby replica and its cross region spread is flat by infrastructure, not by caching. Repeated identical requests return `x-cache: Miss` every time; CloudFront does not cache this endpoint." + - "Quote quality (output amount, net of gas) is not scored here. Two providers can return the same time to first byte while one quotes a noticeably worse price. Converting all outputs to a comparable USD value introduces an oracle bias the leaderboard does not want to bake into a latency ranking." + - "Long tail token rotation defeats per pair edge caches but biases the comparison toward providers whose route engine handles fresh mints quickly. A pure SOL to USDC trader would see different absolute numbers; relative ordering on the cohort is the integration grade signal." + - "Direct egress from Railway, no residential proxy. The bench measures backend to backend latency as a real integration sees it; a wallet calling the API from a mobile network adds the mobile network's RTT on top." + +faq: + - q: "Which Solana DEX has the fastest quote API right now?" + a: "{{best_name}} currently returns quotes the fastest at {{best_p50}} (p50, 24h) across {{count}} measured providers. The leaderboard re-sorts every 60 seconds against fresh samples and rotates the target token each tick so no provider can serve from edge cache. The answer reflects 24 hours of measured latency across us-east, eu-west and Singapore." + - q: "What is quote latency on a DEX aggregator?" + a: "Quote latency is the wall clock time between a swap UI asking a DEX aggregator 'what would I get if I traded 100 USDC for this token right now' and the aggregator answering with a usable routed price. A quote API at 80 ms feels instant; a quote API at 600 ms introduces a visible delay between input change and updated output. The number sets the floor on how live a swap UI can feel before any other latency (RPC, signing, broadcast) is added." + - q: "Why does the bench rotate the target token instead of always quoting SOL to USDC?" + a: "SOL to USDC is the most cached pair on Solana. Jupiter's lite endpoint serves it from a CloudFront edge in 30 to 50 ms, but that measures cache hit, not routing. Rotating the target every tick against the tokens actually trending right now via Mobula Pulse V2's bonded WebSocket feed defeats every per pair edge cache and forces each provider to actually run a routing search. The metric becomes a fair comparison of routing engines, not CDN configurations." + - q: "What if a provider cannot quote a particular long tail token?" + a: "It is counted as a no route, not as a slow quote. Each provider has a recognisable 'I have no path for this pair' signal (Jupiter NO_ROUTES_FOUND, Mobula 'No route found', Raydium INSUFFICIENT_LIQUIDITY, OpenOcean payload level). When the harness sees it, the tick lands on `solana_quote_no_route_total` and is excluded from the latency histogram entirely, so providers that fail fast on coverage gaps are not penalised on the percentiles." + - q: "Why does Raydium have a much lower success rate than the aggregators?" + a: "Raydium's compute endpoint is single venue: it only routes against Raydium's own AMM v4, CPMM and CLMM pools and does not multi hop via SOL the way Jupiter or Mobula do. Because the rotation pulls from Mobula Pulse V2's bonded feed (most of which are Pump.fun graduates living on PumpSwap, plus Meteora and Orca), Raydium returns INSUFFICIENT_LIQUIDITY or ROUTE_NOT_FOUND on roughly 80% of picks. The Raydium latency column is the conditional p50 over the small subset of bonded tokens that happen to have a Raydium pool." + - q: "Are authentication errors and rate limits counted as slow?" + a: "No. HTTP 401, 403 and 429 responses are excluded from the latency histogram entirely. They are counted in separate counters (`solana_quote_auth_error_total`, `solana_quote_throttled_total`) which show up on the success rate column. A provider that is fast but rate limits at the configured cadence loses points on success rate, not on latency." + +related: + - which-evm-aggregator-has-the-fastest-quote + - which-solana-rpc-lands-the-most-transactions + - which-crypto-price-api-is-the-fastest + +seo_title: "Which Solana DEX aggregator returns the fastest swap quote in 2026?" +seo_description: "{{best_name}} leads Solana DEX quote latency at {{best_p50}} (p50, 24h) across Jupiter, Mobula, OpenOcean and Raydium on a rotating trending token basket. Measured live by OpenChainBench from 3 regions." +status: live diff --git a/benchmarks/bridge-revenue.yml b/benchmarks/bridge-revenue.yml new file mode 100644 index 000000000..61c6a80e3 --- /dev/null +++ b/benchmarks/bridge-revenue.yml @@ -0,0 +1,188 @@ +# OpenChainBench. Bench № 028 + +slug: bridge-revenue +number: "028" +title: Cross-chain bridge implied protocol revenue +seo_title: "Bridge revenue tracker 2026: cross-chain protocol implied margin USD per day, take rate bps, swap volume (Relay + comparable bridges)" +seo_description: "Live cross-chain bridge implied protocol revenue. (USD in − USD out − gas − app fees) summed over swaps in 24h / 7d / 30d. Upper bound on what each bridge + solvers keep. Relay tracked today; Across / Socket / deBridge to follow. Methodology pre-registered, harness OSS." +subtitle: Implied margin in USD that each cross-chain bridge plus its solver network collectively retain per swap, summed over rolling 24h / 7d / 30d windows. Upper bound, not exact. Read methodology before citing. +category: Bridges +status: live +metric: Implied revenue USD (24h) +unit: usd +higher_is_better: true + +disclaimer: | + This is an UPPER BOUND, not an exact protocol-revenue number. The margin = USD-in minus USD-out minus gas minus disclosed app fees. Some of that margin Relay shares with solvers in ways we cannot distinguish from the public swap data, so the bench attributes the full amount to "Relay plus solvers collectively". Treat the headline as a ceiling on Relay's own take, not a P&L line. Unpriced swaps (token without a USD reference at swap time) are excluded from the sum and counted separately. + +seo_intro: | + This benchmark answers a question that has no honest public + answer today. how much money does Relay.link, the leading + cross-chain swap routing protocol, actually retain from the + flow it processes. The bench computes implied margin per swap + as (USD value the user paid) minus (USD value the user + received) minus (gas total in USD) minus (any external app + fees the API discloses), then sums that margin over rolling + 24-hour, 7-day and 30-day windows. The result is the smallest + possible number consistent with the on-chain swap data, but + not necessarily the smallest possible number consistent with + Relay's books. Relay shares revenue with the solver network + in ways the public API does not break out, so the bench + attributes the full margin to "Relay plus solvers collectively". + Treat the headline as a ceiling on what Relay alone keeps. + Why publish an upper bound. Because no lower bound exists in + public data. Relay does not publish a fee schedule mapping + volume to revenue. Solver-rebate arrangements are private. + Community-tracked volume ($80M-$150M per day per public + disclosures) times a market-implied 1-5 bps blended take + rate yields an expected band of $20k-$100k per day, or + $0.75M-$2.5M per month. This bench tightens that band by + measuring directly from the swap stream and discloses every + methodology choice so readers can reproduce or argue with + the number. + How it works. A Go program polls the Relay.link public swap + API every 60 s, joins each completed swap against a USD + reference at the swap timestamp, subtracts gas and disclosed + external app fees, and emits the resulting margin as a + Prometheus gauge labeled by window. Unpriced swaps are + excluded from the sum and counted separately. Headline + numbers render as p50 / p90 / p99 on the bench page; those + slots are repurposed for 24h / 7d / 30d windows of the same + margin metric. Take rate in bps is published alongside. + +abstract: | + Relay.link is a cross-chain swap routing protocol that + publishes per-swap detail through a public API. This + benchmark consumes that API on a fixed cadence, computes + per-swap implied margin in USD as (input USD value) minus + (output USD value) minus (gas USD) minus (disclosed external + app fees), and sums the margin over rolling 24-hour, 7-day + and 30-day windows. The result is published as the protocol- + revenue upper bound. Volume USD, swap count and the implied + take rate in basis points are surfaced alongside as context. + The bench does not have access to Relay's internal solver- + rebate accounting, so the number is interpreted as the + margin retained collectively by Relay plus its solver + network, not Relay's own bottom-line revenue. Cross- + validation. Public disclosures put Relay volume at $80M to + $150M per day. Bridge / aggregator take rates cluster at 1 + to 5 basis points blended. Multiplying yields an expected + protocol-revenue band of $20k to $100k per day, or $0.75M + to $2.5M per month, which the live bench number should fall + inside. Significant deviation is either a data-quality issue + the methodology section flags (price-feed gap, unpriced + swap surge, API change) or genuine signal that solver share + has shifted. Limitations. The margin definition is an upper + bound. Solver rebates would lower it; data-quality gaps + (missing USD reference at swap time, gas estimation drift, + undisclosed app fees) would lower the published headline + relative to true protocol revenue. The harness publishes + unpriced-swap count and last-poll timestamp as freshness + signals so readers can spot data-quality regressions. + +methodology: + - "Data source. The Relay.link public swap API is polled on a fixed cadence (default 60 s). The harness consumes the swap stream incrementally, deduping by swap ID, and emits each completed swap as a single contribution to the running window sums. Only swaps with status `success` are counted; pending or failed swaps are tracked separately and excluded from the revenue sum." + - "Margin definition. per_swap_margin_usd = input_usd - output_usd - gas_usd - external_app_fees_usd. input_usd and output_usd are computed by joining the on-chain token amounts against a USD reference at the swap timestamp from CoinGecko or equivalent. gas_usd is the chain-native gas cost (gas_used * gas_price) converted to USD at the same timestamp. external_app_fees_usd is the sum of any frontend or referrer fees the swap payload discloses." + - "Window sums. relay_revenue_usd{window=\"24h\"} is the rolling 24-hour sum of per-swap margin. relay_revenue_usd{window=\"7d\"} is the rolling 7-day sum. relay_revenue_usd{window=\"30d\"} is the rolling 30-day sum. relay_volume_usd and relay_swap_count are computed the same way over the same windows. The bench page binds the three timescales to the p50 / p90 / p99 slots respectively, so the leaderboard surfaces all three at a glance." + - "Take rate. relay_take_rate_bps = (relay_revenue_usd{window=\"24h\"} / relay_volume_usd{window=\"24h\"}) * 10000. Reported on the 24-hour window as the most recent operational signal. Expected band per cross-validation is 1-5 bps blended; a take-rate reading outside that band is either a data-quality issue or a regime change worth investigating." + - "Pricing coverage. relay_swaps_priced_total counts swaps where every leg had a USD reference at swap time. relay_swaps_unpriced_total counts swaps where at least one leg did not (long-tail token, missing reference, price-feed gap). The success column on the bench page is priced / (priced + unpriced), a data-quality ratio rather than a service-uptime ratio. A low ratio means the published headline understates true volume and margin." + - "Why margin is an upper bound on protocol revenue. Relay shares revenue with its solver network through arrangements the public API does not break out. Per-swap margin therefore captures what Relay AND solvers collectively retain, not Relay alone. Without access to internal rebate accounting, the bench cannot decompose the split. Headline is published as 'Relay implied protocol revenue (upper bound)'. Readers building a P&L model should apply their own solver-share assumption to the headline." + - "Freshness. relay_last_poll_timestamp_seconds is a Unix-timestamp gauge of the most recent successful API poll. The bench page's success indicator and any cron alerter use this to detect API outages or harness stalls. expected_freshness_seconds is set to 300 (5 min) which is 5x the 60 s poll cadence, so one missed cycle is tolerated and two missed cycles fire." + - "Cross-validation expected band. Public disclosures put Relay daily volume at $80M to $150M per day. Industry-standard cross-chain take rates cluster at 1 to 5 basis points blended. Multiplying the two ranges yields an expected revenue band of $20k to $100k per day, or $0.75M to $2.5M per month. The live number should fall inside this band. Significant deviation is either a data-quality issue or genuine signal that solver share has shifted." + - "Reproducibility. The full harness source is published in the OpenChainBench harnesses directory (Go). Anyone can clone, set the Relay.link API endpoint env var, run the binary against a Prometheus scraper, and reproduce these metrics. The bench does not rely on any private Mobula service for measurement, the only external dependency is the Relay.link public API and a public USD price reference (CoinGecko or equivalent)." + - "Methodology versioning. Any change to the margin definition, window sizes, pricing source, or excluded-swap rules ships as a public PR with a 14-day comment window. Major changes (e.g. switching from upper-bound margin to a solver-net-decomposed model once private data becomes available) run a 30-day shadow period publishing old and new metric series in parallel." + +findings: + - "Relay implied protocol revenue (24h, upper bound) is currently {{p50:relay}}. Sum of (USD paid - USD received - gas - disclosed app fees) across every successful swap in the last 24 hours. This is the smallest number consistent with the public on-chain swap data and an upper bound on what Relay alone retains after solver-share rebates." + - "The 7-day rolling sum sits at {{p90:relay}}. Smoothing across a week removes day-of-week volume effects and the noise from a single large arbitrage swap that can dominate a 24-hour window. Read this number as the steady-state protocol-revenue ceiling." + - "The 30-day rolling sum is {{p99:relay}}. Monthly recurring revenue ceiling, useful for comparing against public revenue disclosures from comparable cross-chain protocols and against the $0.75M-$2.5M per month expected band derived from volume times take-rate cross-validation." + - "Implied take rate over the last 24 hours is approximately (relay_revenue_usd / relay_volume_usd) * 10000 basis points. Expected blended band per industry cross-validation is 1 to 5 bps. A reading outside this band is either a data-quality issue (priced-swap ratio dropping, gas-estimation drift) or genuine signal that solver share has shifted." + - "Pricing coverage (priced swaps / total swaps over 24h) controls how much of true volume the headline captures. A 100 % ratio means every swap was joined against a USD reference and the headline reflects the full flow. A lower ratio means long-tail tokens are dropping out of the margin sum and the published number understates true protocol revenue proportionally." + +faq: + - q: "Is this Relay.link's actual revenue?" + a: "No. It is an upper bound. Implied margin equals USD paid minus USD received minus gas minus disclosed app fees, summed over completed swaps. Relay shares an unknown fraction of that margin with its solver network through arrangements the public API does not break out. The headline therefore captures what Relay AND its solver network collectively retain, not Relay's own bottom-line revenue. Without access to internal rebate accounting, the bench cannot decompose the split. Treat the headline as a ceiling and apply your own solver-share assumption if you need a Relay-only number." + - q: "Why publish an upper bound instead of an exact number?" + a: "Because no exact number exists in public data and an upper bound is the most rigorous estimate the public swap stream supports. Relay does not publish a fee schedule mapping volume to protocol revenue, and solver-rebate arrangements are private. The alternatives are either to publish nothing, or to publish a guess. An upper bound computed directly from the swap data is more useful than a guess, and is honest about the residual uncertainty. The methodology section spells out exactly what the upper-bound assumption is so readers can apply their own adjustments." + - q: "What does the expected band of $20k-$100k per day come from?" + a: "Cross-validation. Public disclosures put Relay daily volume at $80M to $150M per day. Industry-standard cross-chain take rates cluster at 1 to 5 basis points blended (bridge aggregators and routing protocols typically sit in this range). Multiplying the two ranges yields an expected revenue band of $20k to $100k per day, or $0.75M to $2.5M per month. The live bench number should fall inside this band. A reading meaningfully outside it is either a data-quality issue the methodology flags, or a signal that the cross-validation assumptions need to be revisited." + - q: "Why are 24h, 7d and 30d shown in the p50 / p90 / p99 columns?" + a: "OpenChainBench's standard leaderboard renders p50 / p90 / p99 of a latency-like metric. For a revenue benchmark there is no native percentile interpretation, so the three slots are repurposed to surface the same metric at three timescales: 24h in p50, 7d in p90, 30d in p99. This preserves the at-a-glance comparability of the leaderboard while making the multi-window nature of the metric explicit. The methodology section calls this out so readers do not misread the columns as actual percentiles." + - q: "What happens to swaps whose tokens have no USD price at swap time?" + a: "They are excluded from the margin sum and the volume sum, and counted separately as relay_swaps_unpriced_total. The success column on the bench page is priced / (priced + unpriced), so a low ratio is visible. Causes of an unpriced swap include long-tail token without a CoinGecko entry, price-feed gap, or a brand-new token launched within minutes of the swap. The published headline understates true volume and margin proportionally to the unpriced share, so a 95 % priced ratio means the headline is about 5 % low." + - q: "Why is the freshness threshold 5 minutes?" + a: "The harness polls Relay's public API every 60 seconds. expected_freshness_seconds is set to 5 minutes which is 5x the poll cadence, so one missed cycle is tolerated and roughly two missed cycles fire the cron alerter. Setting it tighter (e.g. 90 s) would cause spurious alerts on transient API hiccups; setting it looser (e.g. 30 min) would let a multi-cycle outage go unnoticed. 5 min is the standard balance for a 1-min-cadence harness in this repo." + - q: "Could solver rebates make the real number much lower than the headline?" + a: "Yes. If Relay rebates half of every basis point of margin back to the solver that filled the swap, the Relay-only revenue is half the headline. Public disclosures from comparable protocols (e.g. CowSwap, 1inch Fusion) suggest solver-share fractions in the 30 % to 70 % range depending on intent design. The bench does not pick a number for Relay because that would be a guess. Readers building a P&L model should apply their own solver-share assumption to the headline. The headline itself is the ceiling, the floor is open." + - q: "Why is this bench in the Bridges category?" + a: "Relay.link is a cross-chain swap routing protocol, the closest fit in the OpenChainBench category list is Bridges. Other benches in this category measure bridge fees, bridge quote latency and bridge-monitor finality. A Trading or Aggregators classification would also be defensible since Relay is structurally closer to an intent-settlement layer than a classic bridge, but Bridges is the most discoverable bucket for the readers most likely to want this number." + +source: https://github.com/ChainBench/OpenChainBench/blob/main/benchmarks/bridge-revenue.yml +# The Relay harness lives in the private mobula-api/miniapps tree for +# now; this spec self-links until the harness is ported to the public +# OCB repo. Additional bridges (across-revenue, socket-revenue) will +# land as sibling harnesses and feed their own provider entries into +# this same bench spec. + +prometheus: + window: 24h + expected_freshness_seconds: 300 + +# Real metrics emitted by the Relay.link revenue harness: +# relay_revenue_usd{window} gauge (window: 24h | 7d | 30d) +# relay_volume_usd{window} gauge (window: 24h | 7d | 30d) +# relay_swap_count{window} gauge (window: 24h | 7d | 30d) +# relay_take_rate_bps gauge (24h window only, derived margin / volume * 10000) +# relay_swaps_priced_total counter +# relay_swaps_unpriced_total counter +# relay_last_poll_timestamp_seconds gauge +# +# Mapping to leaderboard columns: +# p50 = relay_revenue_usd{window="24h"} (latest 24h window) +# p90 = relay_revenue_usd{window="7d"} (rolling 7d window) +# p99 = relay_revenue_usd{window="30d"} (rolling 30d window) +# mean = relay_revenue_usd{window="7d"} / 7 (daily avg over 7d) +# series = relay_revenue_usd{window="24h"} (chart, evolves over time) +# sample_size = relay_swap_count{window="24h"} +# success = priced / (priced + unpriced) (data-quality ratio) +# +# Note: the p50 / p90 / p99 slots here are repurposed for the 24h / 7d / 30d +# windows because a revenue gauge has no native percentile interpretation. +# This is called out in the methodology and the FAQ. + +providers: + - slug: relay + name: Relay + tag: Cross-chain swap routing protocol + formula: "Default headline = ceiling: sum over 24h of (input USD − output USD − gas USD − app fees) per successful swap. Toggle Floor for the on-chain solver-wallet stablecoin delta." + type: relay + secondary: + label: Take rate (24h, bps) + value: "see relay_take_rate_bps" + queries: + p50: relay_revenue_usd{window="24h"} + p90: relay_revenue_usd{window="7d"} + p99: relay_revenue_usd{window="30d"} + mean: relay_revenue_usd{window="7d"} / 7 + success: sum(increase(relay_swaps_priced_total[24h])) / (sum(increase(relay_swaps_priced_total[24h])) + sum(increase(relay_swaps_unpriced_total[24h]))) + sample_size: relay_swap_count{window="24h"} + series: relay_revenue_usd{window="24h"} + +# Two switchable metric panels above the chart so readers can flip between +# the public-API ceiling (the default headline) and the on-chain solver- +# wallet floor. The actual revenue Relay retains lives between the two. +metric_panels: + - id: ceiling + label: Ceiling (implied margin, 24h) + metric: relay_revenue_usd{window="24h"} + label_key: provider + unit: usd + higher_is_better: true + description: "Upper bound from the public Relay swap API. Sum over 24h of input minus output minus gas minus disclosed app fees. Includes solver and integrator share." + - id: floor + label: Floor (solver stablecoin delta, 24h) + metric: relay_solver_balance_delta_usd{kind="stables",window="24h"} + label_key: provider + unit: usd + higher_is_better: true + description: "Lower bound from the Relay solver EOA stablecoin balance change over 24h, sampled from Mobula's wallet portfolio every 5 min. What the system actually retained before any sweep to ops." diff --git a/benchmarks/evm-quote-latency.yml b/benchmarks/evm-quote-latency.yml new file mode 100644 index 000000000..a8ae5d3ff --- /dev/null +++ b/benchmarks/evm-quote-latency.yml @@ -0,0 +1,194 @@ +# OpenChainBench. Bench № 033 + +slug: evm-quote-latency +number: "033" +title: Fastest EVM swap quote API +seo_title: "Fastest EVM swap quote API in 2026: Mobula, KyberSwap, Bebop, CoW, Enso, LI.FI, OpenOcean live latency benchmark" +seo_description: "Live p50/p90/p99 round trip latency for the major EVM swap quoting APIs measured every minute on a rotating basket of top markets across Ethereum, Base, Arbitrum and BSC, with a 3 region geographic split." +subtitle: Wall clock p50 latency of GET /quote per provider, measured every minute on a 5 pair basket across 4 chains. + +per_chain_explainer: + - slug: ethereum + h2: "Fastest Ethereum swap quote API" + body: | + {{best_name:chain:ethereum}} currently leads the Ethereum swap-quote field at {{best_p50:chain:ethereum}} (p50, 24h) across the 7 measured providers. Ethereum mainnet holds the deepest EVM liquidity (Uniswap v3 tick-range pools, Curve stable pools, Maker PSM), so route-search complexity is the dominant component of quote latency: aggregators trading off depth-of-search against time-to-first-byte show their architecture here more than on any other chain. Probes run every 60 seconds on a 5-pair basket from us-east, eu-west and Singapore. + - slug: base + h2: "Fastest Base swap quote API" + body: | + {{best_name:chain:base}} currently leads the Base swap-quote field at {{best_p50:chain:base}} (p50, 24h). Base liquidity is dominated by Aerodrome's ve(3,3) gauges and Uniswap v3, with a thinner long tail than Ethereum, so quote engines that pre-warm a per-chain pool graph (KyberSwap, Mobula) have a structural edge against bridge-first aggregators that re-resolve routes per request. Probes run from us-east, eu-west and Singapore on a 0.5 ETH to USDC pair. + - slug: arbitrum + h2: "Fastest Arbitrum swap quote API" + body: | + {{best_name:chain:arbitrum}} currently leads the Arbitrum swap-quote field at {{best_p50:chain:arbitrum}} (p50, 24h). Arbitrum One concentrates liquidity in Camelot v3, Uniswap v3 and Curve, with sub-second sequencer blocks that let quote engines using `eth_call` simulation refresh their price source between every probe. Bebop's RFQ model resolves price through a maker network rather than via on-chain simulation. Probes run every 60s on a 100 USDC to ETH pair from three regions. + - slug: bsc + h2: "Fastest BSC swap quote API" + body: | + {{best_name:chain:bsc}} currently leads the BSC swap-quote field at {{best_p50:chain:bsc}} (p50, 24h). BSC liquidity is concentrated in PancakeSwap v2 and v3, with a long memecoin tail that pushes aggregators toward exhaustive route enumeration. CoW Protocol is absent because CoW Settlement does not deploy on BSC. Probes fire every 60 seconds on a 1 BNB to USDT pair from us-east, eu-west and Singapore against the public sequencer RPC. + +category: Aggregators +status: live +metric: Quote latency +unit: ms +higher_is_better: false + +seo_intro: | + EVM swap aggregator APIs all promise sub second quotes. This benchmark + measures how often they actually deliver one. The harness rotates a 5 + pair basket across Ethereum, Base, Arbitrum and BSC, fires a quote + request at every supported provider in parallel every 60 seconds, and + records the wall clock round trip. The leaderboard sorts by p50 latency + over the last 24 hours, lower is better. A geographic split is also + available, with the same probe running independently from us east, eu + west and Singapore so a wallet integrator can pick the provider whose + edge gateway is closest to their backend. + +abstract: | + Each provider's public quote endpoint is hit on five fixed pairs + (1 ETH to USDT on Ethereum, 1000 USDC to USDT on Ethereum, 0.5 ETH to + USDC on Base, 100 USDC to ETH on Arbitrum, 1 BNB to USDT on BSC), + rotating one pair per 60 second tick. Per (provider, chain) we land + roughly 288 samples in 24 hours, enough to put p90 and p99 buckets on a + solid base. Quote quality (output amount, net of gas) is not scored, + only latency. + +methodology: + - "Five fixed pairs across four chains. Polygon and Optimism will be added in v2 once they reach a comparable sample volume." + - "60 second tick, one pair per tick, round robin. Every (provider, chain) sees a quote every 5 minutes." + - "Three regions (us east, eu west, sgp). Each is a separate Railway service writing the same metric family with a region label, so the page can filter to a single edge or aggregate across all three." + - "Latency is wall clock from request dispatch to last byte received, observed only on the happy path (HTTP 2xx and a parseable output amount). Failures land on dedicated counters (auth, throttle, no route, other) and pull the success gauge to 0." + - "Native asset sentinel is the canonical 0xEeeeeEeee form. Odos uses 0x0000... and Bebop substitutes the wrapped equivalent on the sell side. The adapter normalises per provider." + - "Provider auth: Mobula requires the sponsor key in an Authorization header. KyberSwap, Bebop, LI.FI and OpenOcean accept anonymous traffic." + +findings: + - "{{best_name}} currently leads at {{best_p50}} on the active chain tab, across {{count}} measured providers. Rankings shift per chain tab: a provider's edge placement and per-chain engine split matter more than its global average." + - "{{name:kyberswap}} returns {{p50:kyberswap}} (p50, 24 h). Its aggregator engine is split per chain and the no-auth path keeps the HTTP round trip lean." + - "{{name:openocean}} ({{p50:openocean}}) and {{name:lifi}} ({{p50:lifi}}) trade latency for breadth: both cover 30+ chains, and the route search across many underlying venues dominates their latency curve." + - "Bebop is RFQ. The output amount it quotes is already net of fees because settlement is gasless for the taker. The latency you see is the maker network resolving the price, not a public mempool route search." + +faq: + - q: "Which EVM swap quote API has the lowest latency right now?" + a: "{{best_name}} currently leads at {{best_p50}} (p50 over the last 24 hours) on the active tab, across {{count}} measured providers. The leaderboard re sorts every minute on fresh Prometheus samples, so the answer reflects measured latency on the live basket, not a marketing claim." + - q: "Does this measure quote quality or just speed?" + a: "Speed only. Latency is wall clock round trip from request to last byte, recorded only on a successful parseable response. Output amount in USD is not scored on this bench because providers measure it differently (Bebop is net of fees on a gasless RFQ, others are gross), and converting all outputs to a comparable USD value introduces an oracle bias we do not want to bake into a latency leaderboard." + - q: "Why are some providers not on every chain?" + a: "Each provider declares its supported chain set. The harness skips probes for chains a provider does not claim to cover so the failure counter is not inflated by structural non coverage." + - q: "What happens when a provider rate limits the harness?" + a: "Each adapter classifies the response. 429 lands on a throttle counter and the success gauge drops to 0. 401 and 403 land on an auth counter. Empty or zero output amounts on a 200 land on a no route counter. None of those failure modes contribute to the latency histogram, so the p50 number stays representative of healthy quotes." + - q: "How is the geographic split implemented?" + a: "Three identical Railway services run the harness with different MONITOR_REGION env vars (us east, eu west, sgp). Each writes the same metric family with a region label. The bench page filter on region selects which slice of the histogram is queried." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/evm-swap-quoting + +prometheus: + window: 24h + expected_freshness_seconds: 600 + freshness_metric: evm_swap_quote_latency_ms_count + +dimensions: + chain: + - { value: all, label: All chains } + - { value: ethereum, label: Ethereum } + - { value: base, label: Base } + - { value: arbitrum, label: Arbitrum } + - { value: bsc, label: BSC } + region: + - { value: all, label: All regions } + - { value: us-east, label: US East } + - { value: eu-west, label: EU West } + - { value: sgp, label: Singapore } + +# Exact per-cell (chain x region) rankings for scoped badges. One query, +# one sample per (provider, chain, region) cell. +rank_matrix_query: avg by (provider, chain, region) (ocb:evm_swap_quote_latency_ms:p50_24h) + +providers: + - slug: mobula + name: Mobula + tag: API key required, dedicated edge + formula: "Avg of per-(chain, region) 24h medians of evm_swap_quote_latency_ms{provider=mobula} over the last 24 hours, only successful 2xx responses with a non zero output amount." + queries: + p50: avg(ocb:evm_swap_quote_latency_ms:p50_24h{provider="mobula"}) + p90: avg(ocb:evm_swap_quote_latency_ms:p90_24h{provider="mobula"}) + p99: avg(ocb:evm_swap_quote_latency_ms:p99_24h{provider="mobula"}) + mean: sum(ocb:evm_swap_quote_latency_ms:sum_rate_24h{provider="mobula"}) / sum(ocb:evm_swap_quote_latency_ms:count_rate_24h{provider="mobula"}) + success: avg(ocb:evm_swap_quote_success:avg_24h{provider="mobula"}) + sample_size: sum(ocb:evm_swap_quote_latency_ms:count_increase_24h{provider="mobula"}) + series: histogram_quantile(0.50, sum(rate(evm_swap_quote_latency_ms_bucket{provider="mobula"}[1h])) by (le)) + + - slug: kyberswap + name: KyberSwap + tag: No auth, per chain aggregator engine + formula: "Avg of per-(chain, region) 24h medians of evm_swap_quote_latency_ms{provider=kyberswap} over the last 24 hours." + queries: + p50: avg(ocb:evm_swap_quote_latency_ms:p50_24h{provider="kyberswap"}) + p90: avg(ocb:evm_swap_quote_latency_ms:p90_24h{provider="kyberswap"}) + p99: avg(ocb:evm_swap_quote_latency_ms:p99_24h{provider="kyberswap"}) + mean: sum(ocb:evm_swap_quote_latency_ms:sum_rate_24h{provider="kyberswap"}) / sum(ocb:evm_swap_quote_latency_ms:count_rate_24h{provider="kyberswap"}) + success: avg(ocb:evm_swap_quote_success:avg_24h{provider="kyberswap"}) + sample_size: sum(ocb:evm_swap_quote_latency_ms:count_increase_24h{provider="kyberswap"}) + series: histogram_quantile(0.50, sum(rate(evm_swap_quote_latency_ms_bucket{provider="kyberswap"}[1h])) by (le)) + + - slug: bebop + name: Bebop + tag: RFQ gasless, EIP 55 checksum required + formula: "Avg of per-(chain, region) 24h medians of evm_swap_quote_latency_ms{provider=bebop} over the last 24 hours." + queries: + p50: avg(ocb:evm_swap_quote_latency_ms:p50_24h{provider="bebop"}) + p90: avg(ocb:evm_swap_quote_latency_ms:p90_24h{provider="bebop"}) + p99: avg(ocb:evm_swap_quote_latency_ms:p99_24h{provider="bebop"}) + mean: sum(ocb:evm_swap_quote_latency_ms:sum_rate_24h{provider="bebop"}) / sum(ocb:evm_swap_quote_latency_ms:count_rate_24h{provider="bebop"}) + success: avg(ocb:evm_swap_quote_success:avg_24h{provider="bebop"}) + sample_size: sum(ocb:evm_swap_quote_latency_ms:count_increase_24h{provider="bebop"}) + series: histogram_quantile(0.50, sum(rate(evm_swap_quote_latency_ms_bucket{provider="bebop"}[1h])) by (le)) + + - slug: lifi + name: LI.FI + tag: 30+ chain coverage, same chain via fromChain equal toChain + formula: "Avg of per-(chain, region) 24h medians of evm_swap_quote_latency_ms{provider=lifi} over the last 24 hours." + queries: + p50: avg(ocb:evm_swap_quote_latency_ms:p50_24h{provider="lifi"}) + p90: avg(ocb:evm_swap_quote_latency_ms:p90_24h{provider="lifi"}) + p99: avg(ocb:evm_swap_quote_latency_ms:p99_24h{provider="lifi"}) + mean: sum(ocb:evm_swap_quote_latency_ms:sum_rate_24h{provider="lifi"}) / sum(ocb:evm_swap_quote_latency_ms:count_rate_24h{provider="lifi"}) + success: avg(ocb:evm_swap_quote_success:avg_24h{provider="lifi"}) + sample_size: sum(ocb:evm_swap_quote_latency_ms:count_increase_24h{provider="lifi"}) + series: histogram_quantile(0.50, sum(rate(evm_swap_quote_latency_ms_bucket{provider="lifi"}[1h])) by (le)) + + - slug: openocean + name: OpenOcean + tag: 40+ chain coverage, numeric chainId in URL + formula: "Avg of per-(chain, region) 24h medians of evm_swap_quote_latency_ms{provider=openocean} over the last 24 hours." + queries: + p50: avg(ocb:evm_swap_quote_latency_ms:p50_24h{provider="openocean"}) + p90: avg(ocb:evm_swap_quote_latency_ms:p90_24h{provider="openocean"}) + p99: avg(ocb:evm_swap_quote_latency_ms:p99_24h{provider="openocean"}) + mean: sum(ocb:evm_swap_quote_latency_ms:sum_rate_24h{provider="openocean"}) / sum(ocb:evm_swap_quote_latency_ms:count_rate_24h{provider="openocean"}) + success: avg(ocb:evm_swap_quote_success:avg_24h{provider="openocean"}) + sample_size: sum(ocb:evm_swap_quote_latency_ms:count_increase_24h{provider="openocean"}) + series: histogram_quantile(0.50, sum(rate(evm_swap_quote_latency_ms_bucket{provider="openocean"}[1h])) by (le)) + + - slug: cow + name: CoW Protocol + tag: No auth, RFQ batch auction, no BSC + formula: "Avg of per-(chain, region) 24h medians of evm_swap_quote_latency_ms{provider=cow} over the last 24 hours. BSC is skipped since CoW Settlement does not deploy there." + queries: + p50: avg(ocb:evm_swap_quote_latency_ms:p50_24h{provider="cow"}) + p90: avg(ocb:evm_swap_quote_latency_ms:p90_24h{provider="cow"}) + p99: avg(ocb:evm_swap_quote_latency_ms:p99_24h{provider="cow"}) + mean: sum(ocb:evm_swap_quote_latency_ms:sum_rate_24h{provider="cow"}) / sum(ocb:evm_swap_quote_latency_ms:count_rate_24h{provider="cow"}) + success: avg(ocb:evm_swap_quote_success:avg_24h{provider="cow"}) + sample_size: sum(ocb:evm_swap_quote_latency_ms:count_increase_24h{provider="cow"}) + series: histogram_quantile(0.50, sum(rate(evm_swap_quote_latency_ms_bucket{provider="cow"}[1h])) by (le)) + + - slug: enso + name: Enso + tag: Anonymous shared bucket, success rate capped near 80 percent + formula: "Avg of per-(chain, region) 24h medians of evm_swap_quote_latency_ms{provider=enso}. Enso uses a global 1rps anonymous bucket so ~20% of probes hit 429 and are dropped. Remaining samples keep p50 and p90 stable." + queries: + p50: avg(ocb:evm_swap_quote_latency_ms:p50_24h{provider="enso"}) + p90: avg(ocb:evm_swap_quote_latency_ms:p90_24h{provider="enso"}) + p99: avg(ocb:evm_swap_quote_latency_ms:p99_24h{provider="enso"}) + mean: sum(ocb:evm_swap_quote_latency_ms:sum_rate_24h{provider="enso"}) / sum(ocb:evm_swap_quote_latency_ms:count_rate_24h{provider="enso"}) + success: avg(ocb:evm_swap_quote_success:avg_24h{provider="enso"}) + sample_size: sum(ocb:evm_swap_quote_latency_ms:count_increase_24h{provider="enso"}) + series: histogram_quantile(0.50, sum(rate(evm_swap_quote_latency_ms_bucket{provider="enso"}[1h])) by (le)) diff --git a/benchmarks/solana-dex-quote-latency.yml b/benchmarks/solana-dex-quote-latency.yml new file mode 100644 index 000000000..1938a52a3 --- /dev/null +++ b/benchmarks/solana-dex-quote-latency.yml @@ -0,0 +1,192 @@ +# OpenChainBench. Bench № 029 + +slug: solana-dex-quote-latency +number: "029" +title: Fastest Solana DEX quote API +seo_title: "Fastest Solana DEX quote API in 2026: Jupiter vs Mobula vs OpenOcean vs Raydium" +seo_description: "Live ms latency benchmark for Solana DEX aggregator quote APIs. Jupiter, Mobula, OpenOcean and Raydium probed every 60s from 3 regions, p50/p90/p99 over a 24h window." +subtitle: Wallclock milliseconds from quote request to usable quote response. +category: Aggregators +status: live +metric: Quote latency +unit: ms + +seo_intro: | + Solana DEX aggregators sit on the hot path of every swap a wallet, + trading UI or routing front-end builds. A quote API that takes 600 ms + pushes a perceptible delay onto the user; a quote API at 80 ms feels + instant. This benchmark measures the wall-clock latency of the + request-response round trip on each major Solana DEX aggregator's + quote endpoint, against the tokens that are *actually trending right + now* on Solana. Every 60 seconds the harness picks a fresh bonded + token from Mobula Pulse V2's live WebSocket feed (post-bonding-curve + graduates from Pump.fun, Meteora, Raydium LaunchLab and friends) and + asks each provider for a 100 USDC → tokenOut quote at 1% slippage. + Rotating against newly-trending tokens defeats every per-pair edge + cache and forces each provider to actually search a route, so the + recorded number reflects routing-search cost rather than a CDN hit. + Liquidity-gap failures (a provider that can't route the picked token + at all) land on their own counter and are excluded from the latency + histogram, so a provider that fails fast on long-tail coverage isn't + penalised on the percentiles. + +abstract: | + We measure the wallclock latency of the Solana DEX aggregator quote APIs + on freshly-trending tokens. Every 60 seconds the harness picks one token + from a sliding 30-minute window of Mobula Pulse V2 bonded events on + Solana, then sends an identical 100 USDC → tokenOut quote (1% slippage, + no fee, no referrer) to each provider from three Railway regions and + records the time from request dispatch to the first byte of a response + that contains a usable quoted output amount. + +methodology: + - "Providers measured: Jupiter, Mobula, OpenOcean, Raydium. (DFlow added when partnership API key is available.)" + - "Canonical request: 100 USDC → tokenOut at 1% slippage. The tokenOut rotates every tick (see below)." + - "Token rotation (Pulse-fed). A persistent WebSocket to Mobula Pulse V2 (bonded view, Solana) maintains a 30-min sliding pool of trending bonded tokens, typically 50-300 active mints. Each tick picks one at random. A REST snapshot fallback (`market/query` volume ranks 6-55) refreshes every 10 min for Pulse outages. In steady state every tick is sourced from the WS feed." + - "Cadence: one quote per provider per region every 60 seconds. 1,440 samples per provider per region per day." + - "Regions: us-east, eu-west, sgp." + - "Latency definition: wallclock from HTTP request dispatch to the first byte of a response body that includes the quoted output amount (Jupiter `outAmount`, Mobula `data.amountOutTokens`, OpenOcean `data.outAmount`, Raydium `data.outputAmount`). The HTTP client reuses TCP + TLS connections across ticks (warm-path), matching how a long-running backend integration actually consumes the API. The bench measures steady-state round-trip cost, not the first-request cold-start penalty." + - "Failure classes. Each non-success outcome lands on exactly one counter and is excluded from the latency histogram: `solana_quote_throttled_total` (HTTP 429), `solana_quote_auth_error_total` (401/403), `solana_quote_no_route_total` (provider can't route the picked token), `solana_quote_other_error_total` (network/timeout/parse). Each provider's no-route signal is verified: Jupiter NO_ROUTES_FOUND, Mobula 'No route found', Raydium INSUFFICIENT_LIQUIDITY, OpenOcean payload-level." + - "Raydium scope. Raydium's compute API is single-venue (Raydium AMM v4 / CPMM / CLMM only) and does not multi-hop via SOL like Jupiter or Mobula. On Pulse-fed bonded tokens (mostly Pump.fun graduates living on PumpSwap, plus Meteora and Orca pools) it returns no-route a large and variable share of the time (30% to 100% depending on the rotation set). That's an honest coverage signal and surfaces on the success-rate column, not as latency outliers." + - "Direct egress from Railway, no residential proxy. The bench measures backend-to-backend latency as a real integration would see it." + - "Jupiter serves quotes from regional pods behind CloudFront (observed `x-region: eu-central-1` from EU, `x-pod-name: jupiter-core-*`), so each probe region reaches a nearby replica and its cross-region spread is flat by infrastructure, not by caching. Repeated identical requests return `x-cache: Miss` every time; CloudFront does not cache this endpoint. The other providers answer from a single origin, which is why their per-region tabs spread with geography." + - "Histogram buckets: 10, 25, 50, 100, 200, 500, 1000, 2000, 5000 ms." + +findings: + - "{{best_name}} leads at {{best_p50}} (p50, 24 h) across {{count}} measured Solana DEX quote APIs. The headline is the cross-region median across a rotating long-tail token target; the per-region tabs below show where each provider's infrastructure actually sits." + - "{{name:jupiter}} returns quotes in {{p50:jupiter}} (p50, 24 h). The number looks impossible until you see the response headers: Jupiter runs regional pods behind CloudFront (`x-region` matches the probe region), so a Railway us-east probe reaches a us-east replica in single-digit RTT and the in-memory route engine adds a few ms. It is a real routing search on every tick (`x-cache: Miss` on repeats), served close." + - "{{name:mobula}} returns quotes in {{p50:mobula}} (p50, 24 h). Mobula's quote API runs a live multi-DEX routing search per request, no per-pair body caching, so the gap to providers that historically cached the canonical 1 SOL → USDC pair closes once the bench rotates targets." + - "{{name:openocean}} returns quotes in {{p50:openocean}} (p50, 24 h). OpenOcean's v4 Solana endpoint runs a separate multi-DEX path engine; the routing search dominates the latency curve on long-tail tokens." + - "{{name:raydium}} is a single-venue compute endpoint (Raydium pools only). It cannot multi-hop via SOL, so on the Pulse-fed rotation it no-routes nearly everything (thousands of no-routes per day against a handful of successes). Below 50 successful quotes in 24h its latency cells go blank rather than reporting a percentile computed on single-digit samples; the success-rate column is its honest score." + - "p99 is the integration-grade number. {{p99:jupiter}} / {{p99:mobula}} / {{p99:openocean}} / {{p99:raydium}} is what a swap UI feels when a region spikes or a provider's route engine churns." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/solana-quote-latency + +prometheus: + window: 24h + freshness_metric: solana_quote_latency_ms_count + +# Region selector, tabs at the top of the page. Server-side injects +# `region="X"` into every PromQL query for the active tab. The special +# value `all` skips the filter and aggregates across every region. +# Values match the raw harness labels emitted by the MONITOR_REGION env +# var on each Railway service. +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: eu-west, label: EU-West } + - { value: sgp, label: Singapore } + +# Exact per-region rankings for scoped badges. One query, one sample per +# (provider, region) cell. +rank_matrix_query: avg by (provider, region) (ocb:solana_quote_latency_ms:p50_24h) + +faq: + - q: "Which Solana DEX has the fastest quote API right now?" + a: "{{best_name}} currently returns quotes the fastest at {{best_p50}} (p50, 24 h) across {{count}} measured providers. The leaderboard re-sorts every 60 seconds against fresh samples and rotates the target token each tick so no provider can serve from edge cache. The answer reflects 24 hours of measured latency across us-east, eu-west and sgp, not a marketing-page claim." + - q: "What is quote latency on a DEX aggregator?" + a: "Quote latency is the wallclock time between a swap UI asking a DEX aggregator 'what would I get if I traded 100 USDC for this token right now?' and the aggregator answering with a usable routed price. A quote API at 80 ms feels instant to the user; a quote API at 600 ms introduces a visible delay between input change and updated output. The number sets the floor on how live a swap UI can feel before any other latency (RPC, signing, broadcast) is added." + - q: "Why does the bench rotate the target token instead of always quoting SOL → USDC?" + a: "SOL → USDC is the most-cached pair on Solana: Jupiter's lite endpoint serves it from a CloudFront edge in 30-50 ms, but that measures cache hit, not routing. Rotating the target every tick, and rotating against the tokens that are *actually trending right now* via Mobula Pulse V2's bonded WebSocket feed, defeats every per-pair edge cache and forces each provider to actually run a routing search. The metric becomes a fair comparison of routing engines, not CDN configurations." + - q: "How is quote latency measured on OpenChainBench?" + a: "A harness in each of three Railway regions (us-east, eu-west, sgp) ticks every 60 seconds: it picks one Solana token from a sliding 30-minute window of bonded tokens emitted by Mobula Pulse V2 over WebSocket, then asks each provider for a 100 USDC → tokenOut quote at 1% slippage in parallel. The HTTP client reuses TCP and TLS connections across ticks, so the recorded number is the steady-state round-trip a long-lived backend integration sees, not the one-off cold-start handshake. The wallclock from request dispatch to the first byte containing a usable quoted output amount is the recorded latency. p50, p90 and p99 are computed per region via Prometheus `histogram_quantile` over 24 hours (precomputed recording rules), then averaged equal-weight across the 3 regions." + - q: "What if a provider can't quote a particular long-tail token?" + a: "It's counted as a no-route, not as a slow quote. Each provider has a recognisable 'I have no path for this pair' signal (Jupiter `NO_ROUTES_FOUND` / `TOKEN_NOT_TRADABLE`, Mobula `No route found` / `Token not found`, Raydium `INSUFFICIENT_LIQUIDITY` / `ROUTE_NOT_FOUND`, OpenOcean payload-level signal). When the harness sees it, the tick lands on `solana_quote_no_route_total` and is excluded from the latency histogram entirely, so providers that fail fast on coverage gaps aren't penalised on the percentiles. The success-rate column reflects the no-route rate honestly." + - q: "Why does Raydium have a much lower success rate than the aggregators?" + a: "Raydium's compute endpoint is single-venue: it only routes against Raydium's own AMM v4 / CPMM / CLMM pools and does not multi-hop via SOL the way Jupiter or Mobula do. Because the rotation now pulls from Mobula Pulse V2's bonded feed (most of which are Pump.fun graduates living on PumpSwap, plus Meteora and Orca pools), Raydium returns INSUFFICIENT_LIQUIDITY or ROUTE_NOT_FOUND on roughly 80% of picks. That's accurate: Raydium's API is a Raydium-pool route engine, not an aggregator. The latency column for Raydium is the conditional p50 over the small subset of bonded tokens that happen to have a Raydium pool." + - q: "Are authentication errors and rate limits counted as 'slow'?" + a: "No. HTTP 401/403/429 responses are excluded from the latency histogram entirely. They are counted in separate counters (`solana_quote_auth_error_total`, `solana_quote_throttled_total`) which show up on the success rate column. A provider that's fast but rate-limits us at the configured cadence loses points on success rate, not on latency." + +providers: + - slug: jupiter + name: Jupiter + tag: DEX aggregator (REST) + formula: "Avg of the 3 regional 24h median wallclock ms on Jupiter's `lite-api.jup.ag/swap/v1/quote` for 100 USDC → at 1% slippage. Recorded only on 200 + non-empty `outAmount`. No-route ticks excluded." + queries: + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="jupiter"}) + p90: avg(ocb:solana_quote_latency_ms:p90_24h{provider="jupiter"}) + p99: avg(ocb:solana_quote_latency_ms:p99_24h{provider="jupiter"}) + mean: sum(ocb:solana_quote_latency_ms:sum_rate_24h{provider="jupiter"}) / sum(ocb:solana_quote_latency_ms:count_rate_24h{provider="jupiter"}) + success: avg(ocb:solana_quote_success:avg_24h{provider="jupiter"}) + sample_size: sum(ocb:solana_quote_latency_ms:count_increase_24h{provider="jupiter"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="jupiter"}[1h]))) + regions: + - region: us-east + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="jupiter", region="us-east"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="jupiter", region="us-east"}[1h]))) + - region: eu-west + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="jupiter", region="eu-west"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="jupiter", region="eu-west"}[1h]))) + - region: sgp + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="jupiter", region="sgp"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="jupiter", region="sgp"}[1h]))) + + - slug: mobula + name: Mobula + tag: DEX aggregator (REST) + formula: "Avg of the 3 regional 24h median wallclock ms on Mobula's `api.mobula.io/api/2/swap/quoting` for 100 USDC → at 1% slippage. Recorded only on 200 + non-empty `data.amountOutTokens`. No-route ticks excluded." + queries: + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="mobula"}) + p90: avg(ocb:solana_quote_latency_ms:p90_24h{provider="mobula"}) + p99: avg(ocb:solana_quote_latency_ms:p99_24h{provider="mobula"}) + mean: sum(ocb:solana_quote_latency_ms:sum_rate_24h{provider="mobula"}) / sum(ocb:solana_quote_latency_ms:count_rate_24h{provider="mobula"}) + success: avg(ocb:solana_quote_success:avg_24h{provider="mobula"}) + sample_size: sum(ocb:solana_quote_latency_ms:count_increase_24h{provider="mobula"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="mobula"}[1h]))) + regions: + - region: us-east + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="mobula", region="us-east"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="mobula", region="us-east"}[1h]))) + - region: eu-west + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="mobula", region="eu-west"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="mobula", region="eu-west"}[1h]))) + - region: sgp + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="mobula", region="sgp"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="mobula", region="sgp"}[1h]))) + + - slug: openocean + name: OpenOcean + tag: DEX aggregator (REST) + formula: "3-region avg of 24h median wallclock ms on OpenOcean's `open-api.openocean.finance/v4/solana/quote` for 100 USDC → at 1% slippage. Recorded only when `data.outAmount > 0` and `dexId >= 0`. No-route ticks excluded." + queries: + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="openocean"}) + p90: avg(ocb:solana_quote_latency_ms:p90_24h{provider="openocean"}) + p99: avg(ocb:solana_quote_latency_ms:p99_24h{provider="openocean"}) + mean: sum(ocb:solana_quote_latency_ms:sum_rate_24h{provider="openocean"}) / sum(ocb:solana_quote_latency_ms:count_rate_24h{provider="openocean"}) + success: avg(ocb:solana_quote_success:avg_24h{provider="openocean"}) + sample_size: sum(ocb:solana_quote_latency_ms:count_increase_24h{provider="openocean"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="openocean"}[1h]))) + regions: + - region: us-east + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="openocean", region="us-east"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="openocean", region="us-east"}[1h]))) + - region: eu-west + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="openocean", region="eu-west"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="openocean", region="eu-west"}[1h]))) + - region: sgp + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="openocean", region="sgp"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="openocean", region="sgp"}[1h]))) + + - slug: raydium + name: Raydium + tag: AMM (single-venue Trade API) + formula: "Avg of the 3 regional 24h median wallclock ms on Raydium's compute/swap-base-in for 100 USDC → , recorded only on success. Single-venue; no-route rate varies with the rotation, up to 100% (then it drops off the board)." + queries: + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="raydium"}) and on() (sum(ocb:solana_quote_latency_ms:count_increase_24h{provider="raydium"}) > 50) + p90: avg(ocb:solana_quote_latency_ms:p90_24h{provider="raydium"}) and on() (sum(ocb:solana_quote_latency_ms:count_increase_24h{provider="raydium"}) > 50) + p99: avg(ocb:solana_quote_latency_ms:p99_24h{provider="raydium"}) and on() (sum(ocb:solana_quote_latency_ms:count_increase_24h{provider="raydium"}) > 50) + mean: (sum(ocb:solana_quote_latency_ms:sum_rate_24h{provider="raydium"}) / sum(ocb:solana_quote_latency_ms:count_rate_24h{provider="raydium"})) and on() (sum(ocb:solana_quote_latency_ms:count_increase_24h{provider="raydium"}) > 50) + success: avg(ocb:solana_quote_success:avg_24h{provider="raydium"}) + sample_size: sum(ocb:solana_quote_latency_ms:count_increase_24h{provider="raydium"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="raydium"}[1h]))) + regions: + - region: us-east + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="raydium", region="us-east"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="raydium", region="us-east"}[1h]))) + - region: eu-west + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="raydium", region="eu-west"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="raydium", region="eu-west"}[1h]))) + - region: sgp + p50: avg(ocb:solana_quote_latency_ms:p50_24h{provider="raydium", region="sgp"}) + series: histogram_quantile(0.50, sum by (le) (rate(solana_quote_latency_ms_bucket{provider="raydium", region="sgp"}[1h]))) diff --git a/benchmarks/solana-tx-landing.yml b/benchmarks/solana-tx-landing.yml new file mode 100644 index 000000000..2f905446c --- /dev/null +++ b/benchmarks/solana-tx-landing.yml @@ -0,0 +1,218 @@ +# OpenChainBench. Bench № 016 + +slug: solana-tx-landing +number: "016" +title: Solana transaction landing services market share +seo_title: "Solana tx landing services 2026: Jito, Helius Sender, Nozomi, bloXroute, 0slot, Astralane live market share" +seo_description: "Live observational benchmark of Solana transaction landing services. Market share by tx count, attribution via known on-chain tip wallets for Jito, Helius Sender, Nozomi, bloXroute, 0slot, NextBlock, Astralane, SolanaVibeStation. Zero on-chain footprint, methodology-public." +subtitle: Share of tipped Solana transactions attributed to each landing service, counted via known on-chain tip wallets. Observational, no transactions sent. +category: Trading +status: live +metric: Tx count (24h) +unit: count +higher_is_better: true + +disclaimer: | + Three caveats. (1) Market share, NOT landing rate, measuring success rates would require sending controlled tx, which costs SOL. (2) Jito is over-counted, other services (Helius Sender, Astralane, bloXroute) fan-out tips to Jito alongside their own wallet, inflating Jito's counter. (3) High share ≠ best, Nozomi has tiny share but charges 10x higher tips for guaranteed inclusion. Pick on latency / cost / reliability fit, not leaderboard position. + +seo_intro: | + This benchmark answers the question every Solana dev choosing a + transaction landing service asks. who actually carries the flow, + what does it cost, and is it growing or shrinking. Marketing + pages quote self-reported "99% landing rate" without methodology; + this page measures the reality on-chain by observing the known + tip wallets each service publishes in its docs. Every confirmed + Solana transaction that pays a tip to one of ~72 known landing- + service wallets is attributed to that service. Zero on-chain + footprint, we don't send tx ourselves, we watch the chain. + Coverage. 8 services with cleanly attributable tip wallets. + Jito Block Engine (the OG, 8 wallets), Helius Sender (10 wallets, + disjoint from Jito's pool), Nozomi by Temporal Labs (17 wallets + with `noz` vanity prefix), bloXroute Trader API (17 `bLx` wallets), + 0slot.trade (10 wallets), NextBlock (8 wallets), Astralane Iris, + SolanaVibeStation Lightspeed. Blind spots. Syncro Sender (per- + customer tip wallets, undisclosed), Slipstream (pure router whose + tx land via underlying senders' wallets and get attributed to + those), and any direct-RPC tx that doesn't pay a tip (a large + share of total Solana traffic, but not what this bench measures). + +abstract: | + We attribute Solana transactions to their landing service by + watching the on-chain tip wallets each service documents publicly. + A single WebSocket connection to mainnet-beta subscribes via + `logsSubscribe(mentions=[tip_wallet], commitment=confirmed)` for + each of ~72 tip wallets across 8 services. Every confirmed + notification increments a per-service counter; signatures are + deduplicated against a 50k-entry LRU so reconnect replays don't + double-count. Failed transactions (Err != null) are excluded - + they didn't actually land. The headline metric is total + landed-and-tipped tx attributed to each service in the last 24 + hours, surfaced as a leaderboard sorted by volume. This is the + observational counterpart to a controlled landing-rate benchmark. + it costs nothing to run (no tx sent, no SOL spent), provides + market-share signal that the controlled approach cannot (real + user behaviour, not synthetic probes), and stays neutral + (services can't fingerprint our probes, there are no probes). + Limitations. (a) Helius Sender fans some tx to Jito under the + hood; if Helius's own tip wallet is paid, attribution is clean, + but Helius's "dual-path" fallback to Jito-only payment would be + miscounted as Jito. (b) Services rotating tip wallets without + doc updates introduce silent under-counting until the new + addresses are added to the harness. The harness logs every + unattributable tip-pattern tx so the gap surfaces in operator + metrics. + +methodology: + - "Source: `wss://api.mainnet-beta.solana.com` public WebSocket, no API key. One `logsSubscribe({mentions:[wallet]}, {commitment:confirmed})` call per known tip wallet. Server-side filtered, bandwidth scales with matched traffic, not full firehose." + - "Attribution: 72 tip wallets across 8 services. Jito (8), Helius Sender (10), Nozomi (17), bloXroute (17), 0slot (10), NextBlock (8), Astralane (1), SolanaVibeStation (1). Every signature carries to exactly one service because the wallet sets are disjoint." + - "Dedup: 50,000-entry LRU keyed by signature. Reconnects can replay a few seconds of notifications, without dedup we'd double-count attribution. Memory cost ~4 MB." + - "Failed tx filter: notifications with `err != null` are dropped. The bench measures LANDED tx, not submission attempts. Failed-tx flow is its own measurement and lives outside this bench." + - "Reconnect: WebSocket disconnects (mainnet-beta is best-effort and kicks idle / overloaded clients) trigger exponential backoff 2s → 60s cap. Every (service, subscription) is re-established on reconnect; `solana_landing_reconnects_total` surfaces stability over the day." + - "Blind spots. Syncro Sender (P2P.org) uses per-customer tip wallets not publicly documented, invisible. Slipstream is a router that routes through underlying senders, so its tx are attributed to the underlying service (Jito/Nozomi/etc.). Direct-RPC tx that pay only a priority fee (no tip to a landing service) are not measured, they constitute the majority of Solana volume but they're not the subject of this bench." + - "Excluded by design: time-to-land, success rate per service, geographic latency. Those would require sending controlled tx (active probing) which costs SOL and risks services fingerprinting our probes. This bench is purely observational." + +findings: + - "{{best_name}} currently carries {{best_p50}} of attributed landed tx over the last 24 h, across {{count}} measured services. The current leader's position reflects default integrations: most Solana wallets and trading bots ship with one landing service preconfigured, so flow concentrates on whoever won the integration race rather than on per-tx merit." + - "{{name:helius-sender}} ships {{p50:helius-sender}}. Helius Sender's strength is the dual-path fan-out (Jito + SWQoS staked connections) which gives it second-place reach without owning the volume Jito does." + - "{{name:nozomi}} carries {{p50:nozomi}}. Nozomi has tiny share by volume but historically charges 10x higher tips per tx, used by serious traders who treat the higher tip as guaranteed-inclusion insurance rather than discretionary spend." + - "{{name:bloxroute}} sits at {{p50:bloxroute}}. bloXroute Trader API serves institutional flow with multi-path BDN propagation. Volume share is smaller than its revenue share because individual tips are higher." + - "{{worst_name}} trails at {{worst_p50}}. The long tail (NextBlock, Astralane, SolanaVibeStation, 0slot) collectively carries a few percent of attributed flow. These services compete on specific edges (regional co-location, validator stake size, tip floor) rather than universal coverage." + +faq: + - q: "Why are the p50 / p90 / p99 columns identical?" + a: "This bench has no latency dimension, so the percentile slots are repurposed: every aggregate column carries the same 24h attributed-tx count, and the Success column reports the harness's chain-subscription health rather than any per-service delivery rate. The ranking signal is the count itself (market share); the columns exist because the ledger layout is shared with latency benches." + - q: "What is a Solana landing service?" + a: "Solana has no traditional mempool. Transactions go directly to the current and next slot leader. During congestion, leaders drop transactions they can't process, they're simply lost, not queued. Landing services help your tx avoid being dropped via various tricks. direct connections to leaders (Nozomi, 0slot), stake-weighted QoS pools (Helius Sender, Triton Cascade), tip-based bundle auctions (Jito), multi-path propagation (bloXroute, Astralane). This bench measures which services actually carry the on-chain flow today." + - q: "Why observational? Why not measure landing rate directly?" + a: "Sending controlled tx through each service to measure landing rate costs real SOL (every tx = base fee + tip + compute units, all paid to validators, none recoverable). At a 30 s cadence across 8 services and 3 regions, that's ~$11,700/month. The observational approach costs $0 because we don't send anything, we watch the chain. Trade-off. observational tells you 'who is used' (market share, tip economics, growth); controlled tells you 'who lands best' (per-service success rate). Different questions, both useful. This is the observational answer." + - q: "How is each tx attributed to a service?" + a: "Each landing service publishes a list of known tip wallet addresses in its docs (Jito has 8, Nozomi has 17, etc.). When a user routes a tx through a service, the tx includes a transfer to that service's tip wallet. We watch all 72 tip wallets via `logsSubscribe(mentions=[wallet])` and attribute each notification to the owning service. Wallet sets are disjoint, no overlap between services, so attribution is unambiguous. The pattern is verified against real mainnet blocks: 95-97% of tip-paying tx attribute cleanly, zero multi-service overlaps observed." + - q: "Why does Jito have such dominant share?" + a: "Three reasons. (1) First-mover. Jito launched the tip+bundle pattern in 2022 and got every major wallet (Phantom, Backpack) + every major trading bot to integrate first. (2) Lowest tip floor, Jito accepts tips as low as 1,000 lamports versus 0.001 SOL minimums on most competitors. (3) Atomic bundles, only Jito offers the 5-tx atomic group that DEX aggregators (Jupiter, Raydium) use for sandwich-proof swaps. The result is structural: Jito carries the default flow even when competitors are technically faster on specific paths." + - q: "Why does Nozomi look small here?" + a: "Nozomi's share by tx count is small but its share by tip revenue is much larger. Nozomi targets serious traders who pay 0.001+ SOL per tx as guaranteed-inclusion insurance, while Jito averages ~0.0001 SOL per tx as casual usage. Blockworks Research's 'Solana Block Building Wars' (Feb 2026) documented Nozomi at ~$1M tips on 2M swaps vs Jito's $300k on 24M swaps, a 130x premium per swap. For 'who do serious traders use', track Nozomi. For 'who handles default flow', track Jito." + - q: "What about Syncro Sender and Slipstream?" + a: "Syncro Sender (P2P.org) uses per-customer tip wallets that aren't publicly documented, every customer gets a private address. We can't enumerate them, so Syncro flow is invisible to this bench. Estimated share: small (Syncro launched in 2025 and hasn't disclosed adoption metrics). Slipstream is a router that calls the underlying senders' SDKs, its tx land via Jito/Nozomi/0slot tip wallets and are attributed to those services. So Slipstream-originated flow shows up correctly counted, just under the underlying service's name. If you specifically want to know 'what % uses Slipstream as the router', this bench can't answer that, Slipstream itself doesn't add an identifying memo." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/solana-tx-landing + +prometheus: + window: 24h + +# Real metrics emitted by the solana-tx-landing harness: +# solana_landing_tx_total{service} counter (attributed landed tx) +# solana_landing_subscription_health{service} gauge (0|1) +# solana_landing_reconnects_total counter +# solana_landing_last_slot{service} gauge +# +# Each "provider" below is one landing service. The metric is +# observed throughput (tx/24h), so `higher_is_better: true` - +# the leader is the one carrying the most flow. p50/p90/p99 +# columns are filled with the 24h increase (single scalar) for +# consistency with the OCB renderer; they're not statistical +# distributions because each bench probe is a single counter. + +providers: + - slug: jito + name: Jito + tag: Block Engine, atomic bundles + tip auction, OG since 2022 + formula: "Total count over the last 24h of confirmed mainnet txs that paid a tip to one of Jito's 8 documented tip wallets, deduped via 50k-entry LRU." + queries: + p50: sum(increase(solana_landing_tx_total{service="jito"}[24h])) + p90: sum(increase(solana_landing_tx_total{service="jito"}[24h])) + p99: sum(increase(solana_landing_tx_total{service="jito"}[24h])) + mean: sum(increase(solana_landing_tx_total{service="jito"}[24h])) + success: avg_over_time(solana_landing_subscription_health{service="jito"}[24h]) + sample_size: sum(increase(solana_landing_tx_total{service="jito"}[24h])) + series: sum(rate(solana_landing_tx_total{service="jito"}[5m])) + + - slug: helius-sender + name: Helius + tag: Dual-path fan-out (Jito + SWQoS staked connections), 7 regions + formula: "Total count over the last 24h of confirmed mainnet txs that paid a tip to one of Helius Sender's 10 documented tip wallets, deduped via 50k-entry LRU." + queries: + p50: sum(increase(solana_landing_tx_total{service="helius-sender"}[24h])) + p90: sum(increase(solana_landing_tx_total{service="helius-sender"}[24h])) + p99: sum(increase(solana_landing_tx_total{service="helius-sender"}[24h])) + mean: sum(increase(solana_landing_tx_total{service="helius-sender"}[24h])) + success: avg_over_time(solana_landing_subscription_health{service="helius-sender"}[24h]) + sample_size: sum(increase(solana_landing_tx_total{service="helius-sender"}[24h])) + series: sum(rate(solana_landing_tx_total{service="helius-sender"}[5m])) + + - slug: nozomi + name: Nozomi + tag: Temporal Labs, direct-to-leader, 9 co-located regions, premium tips + formula: "Total count over the last 24h of confirmed mainnet txs that paid a tip to one of Nozomi's 17 `noz`-prefixed tip wallets, deduped via 50k-entry LRU." + queries: + p50: sum(increase(solana_landing_tx_total{service="nozomi"}[24h])) + p90: sum(increase(solana_landing_tx_total{service="nozomi"}[24h])) + p99: sum(increase(solana_landing_tx_total{service="nozomi"}[24h])) + mean: sum(increase(solana_landing_tx_total{service="nozomi"}[24h])) + success: avg_over_time(solana_landing_subscription_health{service="nozomi"}[24h]) + sample_size: sum(increase(solana_landing_tx_total{service="nozomi"}[24h])) + series: sum(rate(solana_landing_tx_total{service="nozomi"}[5m])) + + - slug: bloxroute + name: bloXroute + tag: Multi-path BDN propagation, institutional flow + formula: "Total count over the last 24h of confirmed mainnet txs that paid a tip to one of bloXroute's 17 `bLx`-prefixed tip wallets, deduped via 50k-entry LRU." + queries: + p50: sum(increase(solana_landing_tx_total{service="bloxroute"}[24h])) + p90: sum(increase(solana_landing_tx_total{service="bloxroute"}[24h])) + p99: sum(increase(solana_landing_tx_total{service="bloxroute"}[24h])) + mean: sum(increase(solana_landing_tx_total{service="bloxroute"}[24h])) + success: avg_over_time(solana_landing_subscription_health{service="bloxroute"}[24h]) + sample_size: sum(increase(solana_landing_tx_total{service="bloxroute"}[24h])) + series: sum(rate(solana_landing_tx_total{service="bloxroute"}[5m])) + + - slug: 0slot + name: 0slot + tag: Premium QUIC leader-direct sender + formula: "Total count over the last 24h of confirmed mainnet txs that paid a tip to one of 0slot.trade's 10 documented tip wallets, deduped via 50k-entry LRU." + queries: + p50: sum(increase(solana_landing_tx_total{service="0slot"}[24h])) + p90: sum(increase(solana_landing_tx_total{service="0slot"}[24h])) + p99: sum(increase(solana_landing_tx_total{service="0slot"}[24h])) + mean: sum(increase(solana_landing_tx_total{service="0slot"}[24h])) + success: avg_over_time(solana_landing_subscription_health{service="0slot"}[24h]) + sample_size: sum(increase(solana_landing_tx_total{service="0slot"}[24h])) + series: sum(rate(solana_landing_tx_total{service="0slot"}[5m])) + + - slug: nextblock + name: NextBlock + tag: SWQoS sender, TX Stream API + formula: "Total count over the last 24h of confirmed mainnet txs that paid a tip to one of NextBlock's 8 documented tip wallets, deduped via 50k-entry LRU." + queries: + p50: sum(increase(solana_landing_tx_total{service="nextblock"}[24h])) + p90: sum(increase(solana_landing_tx_total{service="nextblock"}[24h])) + p99: sum(increase(solana_landing_tx_total{service="nextblock"}[24h])) + mean: sum(increase(solana_landing_tx_total{service="nextblock"}[24h])) + success: avg_over_time(solana_landing_subscription_health{service="nextblock"}[24h]) + sample_size: sum(increase(solana_landing_tx_total{service="nextblock"}[24h])) + series: sum(rate(solana_landing_tx_total{service="nextblock"}[5m])) + + - slug: astralane + name: Astralane + tag: Validator co-located, leader-schedule-aware routing + formula: "Total count over the last 24h of confirmed mainnet txs that paid a tip to Astralane Iris's documented tip wallet, deduped via 50k-entry LRU." + queries: + p50: sum(increase(solana_landing_tx_total{service="astralane"}[24h])) + p90: sum(increase(solana_landing_tx_total{service="astralane"}[24h])) + p99: sum(increase(solana_landing_tx_total{service="astralane"}[24h])) + mean: sum(increase(solana_landing_tx_total{service="astralane"}[24h])) + success: avg_over_time(solana_landing_subscription_health{service="astralane"}[24h]) + sample_size: sum(increase(solana_landing_tx_total{service="astralane"}[24h])) + series: sum(rate(solana_landing_tx_total{service="astralane"}[5m])) + + - slug: solanavibestation + name: SolanaVibeStation + tag: Lightspeed validator-pool tip-based sender + formula: "Total count over the last 24h of confirmed mainnet txs that paid a tip to SolanaVibeStation's documented Lightspeed tip wallet, deduped via 50k-entry LRU." + queries: + p50: sum(increase(solana_landing_tx_total{service="solanavibestation"}[24h])) + p90: sum(increase(solana_landing_tx_total{service="solanavibestation"}[24h])) + p99: sum(increase(solana_landing_tx_total{service="solanavibestation"}[24h])) + mean: sum(increase(solana_landing_tx_total{service="solanavibestation"}[24h])) + success: avg_over_time(solana_landing_subscription_health{service="solanavibestation"}[24h]) + sample_size: sum(increase(solana_landing_tx_total{service="solanavibestation"}[24h])) + series: sum(rate(solana_landing_tx_total{service="solanavibestation"}[5m])) From 80690eb82d4b8ace4708c58616da6b8e44d3e1b3 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Fri, 26 Jun 2026 14:53:45 +0200 Subject: [PATCH 02/60] feat(logos): add binance.svg (was using bnb.png from chain registry) --- public/logos/binance.svg | 1 + src/lib/logo-manifest.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 public/logos/binance.svg diff --git a/public/logos/binance.svg b/public/logos/binance.svg new file mode 100644 index 000000000..ecaa7fb4e --- /dev/null +++ b/public/logos/binance.svg @@ -0,0 +1 @@ +Binance \ No newline at end of file diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index 68765d069..a62b8078a 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -100,7 +100,7 @@ const RAW: Record = { "1rpc": "/logos/1rpc.svg", cloudflare: "/logos/cloudflare.svg", "base-official": "/logos/base.jpeg", - binance: "/logos/bnb.png", + binance: "/logos/binance.svg", lava: "/logos/lava.webp", nodies: "/logos/nodies.png", tenderly: "/logos/tenderly.svg", From fe6f261dd23fb57d9a62ef2326e546a71ececd1b Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Fri, 26 Jun 2026 15:03:44 +0200 Subject: [PATCH 03/60] feat(perp-funding-stability): scale to 15 venues + add 8 logos (bench 043) Adds 8 venues sourced via Mobula's CEFI funding-rate aggregator (perp_venue_funding_24h_bps, freshness via perp_venue_last_refresh_unix): - Bitget, Coinbase, Deribit, Gate, Kraken, KuCoin, Lighter, MEXC Same stddev-over-time semantics on a 24h window, BTC/ETH/SOL split per asset. Cohort jumps from 7 to 15 venues. Logos: 5 PNGs sourced from CoinMarketCap exchange CDN (bitget, deribit, gate, kraken, mexc); 2 SVGs from simpleicons (coinbase, kucoin); 1 SVG from simpleicons for binance (was reusing /logos/bnb.png, the chain logo, not the exchange brand). Bench copy + manifest entries updated. --- benchmarks/perp-funding-stability.yml | 124 ++++++++++++++++++++++++-- public/logos/bitget.png | Bin 0 -> 7547 bytes public/logos/coinbase.svg | 1 + public/logos/deribit.png | Bin 0 -> 15506 bytes public/logos/gate.png | Bin 0 -> 16203 bytes public/logos/kraken.png | Bin 0 -> 5879 bytes public/logos/kucoin.svg | 1 + public/logos/mexc.png | Bin 0 -> 12812 bytes src/lib/logo-manifest.ts | 10 +++ 9 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 public/logos/bitget.png create mode 100644 public/logos/coinbase.svg create mode 100644 public/logos/deribit.png create mode 100644 public/logos/gate.png create mode 100644 public/logos/kraken.png create mode 100644 public/logos/kucoin.svg create mode 100644 public/logos/mexc.png diff --git a/benchmarks/perp-funding-stability.yml b/benchmarks/perp-funding-stability.yml index 709b7c008..10f92e7ed 100644 --- a/benchmarks/perp-funding-stability.yml +++ b/benchmarks/perp-funding-stability.yml @@ -3,8 +3,8 @@ slug: perp-funding-stability number: "043" title: Perp DEX funding stability, 24h stddev of ETH funding ranked -seo_title: "Perp funding stability 2026: ETH funding stddev across 7 venues ranked" -seo_description: "Live 24h standard deviation of ETH perpetual futures funding rate in bps across Hyperliquid, Bybit, dYdX v4, Binance, OKX, Paradex and Aster. Lower means more stable funding for carry traders and basis arbitrageurs." +seo_title: "Perp funding stability 2026: ETH funding stddev across 15 venues ranked" +seo_description: "Live 24h standard deviation of ETH perpetual futures funding rate in bps across 15 major venues: Hyperliquid, Bybit, dYdX v4, Binance, OKX, Paradex, Aster, Bitget, Coinbase, Deribit, Gate, Kraken, KuCoin, Lighter, MEXC. Lower means more stable funding for carry traders and basis arbitrageurs." subtitle: 24h standard deviation of ETH funding rate, in basis points per 24h, across the perp-funding cohort. Lower means funding stays in a tight band, the carry trader's preferred signal. category: Trading status: live @@ -24,10 +24,11 @@ seo_intro: | same number. The bench reads the same normalized series as perp-funding (per-venue hold cost in bps per 24h) and exposes stddev_over_time of that series on a 24 hour window for ETH. - Cohort is the 7 venues currently publishing the normalized - hold-24h gauge with the freshness signal: Hyperliquid (onchain), - Bybit, dYdX v4 (Cosmos appchain), Binance, OKX, Paradex (Starknet - L2), Aster (BNB Chain). Lower is better. + Cohort is 15 venues across two sources: 7 polled directly by the + harness (Hyperliquid onchain, Bybit, dYdX v4 Cosmos appchain, + Binance, OKX, Paradex Starknet L2, Aster BNB Chain) and 8 sourced + through Mobula's CEFI funding aggregator (Bitget, Coinbase, + Deribit, Gate, Kraken, KuCoin, Lighter, MEXC). Lower is better. abstract: | The bench reuses the normalized 24h ETH funding series from @@ -43,7 +44,7 @@ methodology: - "Source. Same upstream as perp-funding: each venue's public funding endpoint polled every 60 seconds, normalized to bps per hour and per 24h hold, sign preserved." - "Metric. stddev_over_time(perp_funding_hold_24h_bps{asset=\"ETH\"}[24h]) per venue. The 7d window smooths intraday noise but reacts to regime shifts within a week." - "Sign handling. The underlying series is signed (positive means longs pay). Stability measures the swing, so the ranking uses the unsigned stddev. Average direction and level are the perp-funding bench." - - "Cohort. 7 venues from the perp-funding bench publishing the normalized hold-24h gauge with the freshness signal: Hyperliquid (onchain), Bybit, dYdX v4 (Cosmos appchain), Binance, OKX, Paradex (Starknet L2), Aster (BNB Chain)." + - "Cohort. 15 venues across two upstream sources. 7 polled directly by the harness on each venue's public funding endpoint (perp_funding_hold_24h_bps): Hyperliquid (onchain), Bybit, dYdX v4 (Cosmos appchain), Binance, OKX, Paradex (Starknet L2), Aster (BNB Chain). 8 sourced via Mobula's CEFI funding-rate aggregator (perp_venue_funding_24h_bps): Bitget, Coinbase, Deribit, Gate, Kraken, KuCoin, Lighter, MEXC. Both pipelines normalize to bps per 24h hold with sign preserved, so the unsigned 24h stddev is directly comparable across the full cohort." - "Success. Same freshness signal as perp-funding: perp_funding_venue_ok_unix advancing within the last 5 minutes. A venue whose scrape stalls is tagged stale on the row." - "Cadence. The upstream funding harness polls every 60 seconds; Prometheus scrapes every 30 seconds. The 7 day stddev refreshes on every scrape." - "Failures. A venue whose upstream funding feed errors keeps its last gauge value; the leaderboard tags the row stale once the success gauge drops to 0." @@ -166,3 +167,112 @@ providers: success: (perp_funding_venue_ok_unix{venue="aster"} > bool (time() - 300)) sample_size: count_over_time(perp_funding_hold_24h_bps{venue="aster",asset="ETH"}[24h]) series: stddev_over_time(perp_funding_hold_24h_bps{venue="aster",asset="ETH"}[24h]) + + # ── Mobula-sourced cohort (perp_venue_funding_24h_bps from perp-cohort-stats) ── + # Same metric semantics as perp_funding_hold_24h_bps but normalized + # via Mobula's CEFI funding-rate aggregator instead of direct venue + # polls. Freshness gauge is perp_venue_last_refresh_unix. + + - slug: bitget + name: Bitget + tag: CEX, USDT perpetuals via Mobula aggregator, 8h funding + formula: "stddev_over_time of perp_venue_funding_24h_bps{venue=\"bitget\",asset=\"ETH\"} over a 24h window. Source: Mobula CEFI funding-rate aggregator." + queries: + p50: stddev_over_time(perp_venue_funding_24h_bps{venue="bitget",asset="ETH"}[24h]) + p90: stddev_over_time(perp_venue_funding_24h_bps{venue="bitget",asset="BTC"}[24h]) + p99: stddev_over_time(perp_venue_funding_24h_bps{venue="bitget",asset="SOL"}[24h]) + mean: stddev_over_time(perp_venue_funding_24h_bps{venue="bitget",asset="ETH"}[24h]) + success: (perp_venue_last_refresh_unix{venue="bitget"} > bool (time() - 300)) + sample_size: count_over_time(perp_venue_funding_24h_bps{venue="bitget",asset="ETH"}[24h]) + series: stddev_over_time(perp_venue_funding_24h_bps{venue="bitget",asset="ETH"}[24h]) + + - slug: coinbase + name: Coinbase + tag: Regulated US CEX, perpetuals via Mobula aggregator + formula: "stddev_over_time of perp_venue_funding_24h_bps{venue=\"coinbase\",asset=\"ETH\"} over a 24h window. Source: Mobula CEFI funding-rate aggregator." + queries: + p50: stddev_over_time(perp_venue_funding_24h_bps{venue="coinbase",asset="ETH"}[24h]) + p90: stddev_over_time(perp_venue_funding_24h_bps{venue="coinbase",asset="BTC"}[24h]) + p99: stddev_over_time(perp_venue_funding_24h_bps{venue="coinbase",asset="SOL"}[24h]) + mean: stddev_over_time(perp_venue_funding_24h_bps{venue="coinbase",asset="ETH"}[24h]) + success: (perp_venue_last_refresh_unix{venue="coinbase"} > bool (time() - 300)) + sample_size: count_over_time(perp_venue_funding_24h_bps{venue="coinbase",asset="ETH"}[24h]) + series: stddev_over_time(perp_venue_funding_24h_bps{venue="coinbase",asset="ETH"}[24h]) + + - slug: deribit + name: Deribit + tag: Options-focused CEX, ETH/BTC perpetuals via Mobula aggregator + formula: "stddev_over_time of perp_venue_funding_24h_bps{venue=\"deribit\",asset=\"ETH\"} over a 24h window. Source: Mobula CEFI funding-rate aggregator." + queries: + p50: stddev_over_time(perp_venue_funding_24h_bps{venue="deribit",asset="ETH"}[24h]) + p90: stddev_over_time(perp_venue_funding_24h_bps{venue="deribit",asset="BTC"}[24h]) + p99: stddev_over_time(perp_venue_funding_24h_bps{venue="deribit",asset="SOL"}[24h]) + mean: stddev_over_time(perp_venue_funding_24h_bps{venue="deribit",asset="ETH"}[24h]) + success: (perp_venue_last_refresh_unix{venue="deribit"} > bool (time() - 300)) + sample_size: count_over_time(perp_venue_funding_24h_bps{venue="deribit",asset="ETH"}[24h]) + series: stddev_over_time(perp_venue_funding_24h_bps{venue="deribit",asset="ETH"}[24h]) + + - slug: gate + name: Gate + tag: CEX, USDT perpetuals via Mobula aggregator, 8h funding + formula: "stddev_over_time of perp_venue_funding_24h_bps{venue=\"gate\",asset=\"ETH\"} over a 24h window. Source: Mobula CEFI funding-rate aggregator." + queries: + p50: stddev_over_time(perp_venue_funding_24h_bps{venue="gate",asset="ETH"}[24h]) + p90: stddev_over_time(perp_venue_funding_24h_bps{venue="gate",asset="BTC"}[24h]) + p99: stddev_over_time(perp_venue_funding_24h_bps{venue="gate",asset="SOL"}[24h]) + mean: stddev_over_time(perp_venue_funding_24h_bps{venue="gate",asset="ETH"}[24h]) + success: (perp_venue_last_refresh_unix{venue="gate"} > bool (time() - 300)) + sample_size: count_over_time(perp_venue_funding_24h_bps{venue="gate",asset="ETH"}[24h]) + series: stddev_over_time(perp_venue_funding_24h_bps{venue="gate",asset="ETH"}[24h]) + + - slug: kraken + name: Kraken + tag: Regulated CEX, perpetuals via Mobula aggregator + formula: "stddev_over_time of perp_venue_funding_24h_bps{venue=\"kraken\",asset=\"ETH\"} over a 24h window. Source: Mobula CEFI funding-rate aggregator." + queries: + p50: stddev_over_time(perp_venue_funding_24h_bps{venue="kraken",asset="ETH"}[24h]) + p90: stddev_over_time(perp_venue_funding_24h_bps{venue="kraken",asset="BTC"}[24h]) + p99: stddev_over_time(perp_venue_funding_24h_bps{venue="kraken",asset="SOL"}[24h]) + mean: stddev_over_time(perp_venue_funding_24h_bps{venue="kraken",asset="ETH"}[24h]) + success: (perp_venue_last_refresh_unix{venue="kraken"} > bool (time() - 300)) + sample_size: count_over_time(perp_venue_funding_24h_bps{venue="kraken",asset="ETH"}[24h]) + series: stddev_over_time(perp_venue_funding_24h_bps{venue="kraken",asset="ETH"}[24h]) + + - slug: kucoin + name: KuCoin + tag: CEX, USDT perpetuals via Mobula aggregator, 8h funding + formula: "stddev_over_time of perp_venue_funding_24h_bps{venue=\"kucoin\",asset=\"ETH\"} over a 24h window. Source: Mobula CEFI funding-rate aggregator." + queries: + p50: stddev_over_time(perp_venue_funding_24h_bps{venue="kucoin",asset="ETH"}[24h]) + p90: stddev_over_time(perp_venue_funding_24h_bps{venue="kucoin",asset="BTC"}[24h]) + p99: stddev_over_time(perp_venue_funding_24h_bps{venue="kucoin",asset="SOL"}[24h]) + mean: stddev_over_time(perp_venue_funding_24h_bps{venue="kucoin",asset="ETH"}[24h]) + success: (perp_venue_last_refresh_unix{venue="kucoin"} > bool (time() - 300)) + sample_size: count_over_time(perp_venue_funding_24h_bps{venue="kucoin",asset="ETH"}[24h]) + series: stddev_over_time(perp_venue_funding_24h_bps{venue="kucoin",asset="ETH"}[24h]) + + - slug: lighter + name: Lighter + tag: zkSync L2 perps DEX, native funding via Mobula aggregator + formula: "stddev_over_time of perp_venue_funding_24h_bps{venue=\"lighter\",asset=\"ETH\"} over a 24h window. Source: Mobula CEFI funding-rate aggregator." + queries: + p50: stddev_over_time(perp_venue_funding_24h_bps{venue="lighter",asset="ETH"}[24h]) + p90: stddev_over_time(perp_venue_funding_24h_bps{venue="lighter",asset="BTC"}[24h]) + p99: stddev_over_time(perp_venue_funding_24h_bps{venue="lighter",asset="SOL"}[24h]) + mean: stddev_over_time(perp_venue_funding_24h_bps{venue="lighter",asset="ETH"}[24h]) + success: (perp_venue_last_refresh_unix{venue="lighter"} > bool (time() - 300)) + sample_size: count_over_time(perp_venue_funding_24h_bps{venue="lighter",asset="ETH"}[24h]) + series: stddev_over_time(perp_venue_funding_24h_bps{venue="lighter",asset="ETH"}[24h]) + + - slug: mexc + name: MEXC + tag: CEX, USDT perpetuals via Mobula aggregator, 8h funding + formula: "stddev_over_time of perp_venue_funding_24h_bps{venue=\"mexc\",asset=\"ETH\"} over a 24h window. Source: Mobula CEFI funding-rate aggregator." + queries: + p50: stddev_over_time(perp_venue_funding_24h_bps{venue="mexc",asset="ETH"}[24h]) + p90: stddev_over_time(perp_venue_funding_24h_bps{venue="mexc",asset="BTC"}[24h]) + p99: stddev_over_time(perp_venue_funding_24h_bps{venue="mexc",asset="SOL"}[24h]) + mean: stddev_over_time(perp_venue_funding_24h_bps{venue="mexc",asset="ETH"}[24h]) + success: (perp_venue_last_refresh_unix{venue="mexc"} > bool (time() - 300)) + sample_size: count_over_time(perp_venue_funding_24h_bps{venue="mexc",asset="ETH"}[24h]) + series: stddev_over_time(perp_venue_funding_24h_bps{venue="mexc",asset="ETH"}[24h]) diff --git a/public/logos/bitget.png b/public/logos/bitget.png new file mode 100644 index 0000000000000000000000000000000000000000..0290612bfaa2246cf322b167893734ef0f171199 GIT binary patch literal 7547 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 \ No newline at end of file diff --git a/public/logos/deribit.png b/public/logos/deribit.png new file mode 100644 index 0000000000000000000000000000000000000000..f7be01be934f8b7f12a619bfba43fd55ebebc577 GIT binary patch literal 15506 zcmcJ0%1jNQ0CJN{4hxcL)elN{4hw zUcSF|U)+D-zBv1=v)5kx>}T(1f1Zxj)mA0Kr^QD@LnDH#!StWrga19a*iUVRNo5`y znv5wN1~v4v{F`g~p20XE<7?}YS>w0XRm&mQ!+5ngv(jW3oHap)HP-GZRYnL2_Qyr0 z_u^tu$1q{o2_XTNHmtbx)OhrEc?cN*JkN$2bAVVz9?QS& z$47(D<7EcVeP4-Z9~_AQcije1V-i*U6(QQ@WGu*Z^?R_xDc=4CJ7w{2((XLrp?mwG|8=|ttBHp) ziR}iCuFSaC3M|^tq}O1CMPN{PlMR9p)f*m=-TBC5T&i zZ{mgekULd;w>Z@%QNQglom0Wm-5@2Z-nO^Y(xfns|Mk~Y0pKI3cSNSOA!zaT)QL1s z(j+U^8DkKJvqftVL7`PT#dmz01)4y}psO{Y=77%#7;lfO{cWgjW^2Oo<;v<#pca9! zYf{=moy7Q&2{B6|jV?~YYf2i9?2P%uGwHPO7zzm0*`i1AfG4m9v0-B={l8&2mcSvq zZrA_&m81!ra7*mH%T@h48{(H)%GWsHZ`F_^57t z8$A%d4R%Rk1Lo+!;2)d{tTRXDj4K(u%A@e@p~GLe+|~ef^oP;DfxaUx#bvL86{pi9 zqdOpX7=c*QL)Uk^k?3HF(%Z?>tLuH*72Ce6Yp*kZyw1d?Yp=Fw&&klNBgH{N4Hq@B{AaojJX_dZ!rad&LxRf zy93Mf(HL<$ldg=EV#l`G)H#*eN`E)_cvNWo5$$qG6-j!NnIlV9jC;`($QKiTG%QBQ zk3wMKeSJgN%P!gNLD>HmJ@-YAIul|K&H0c%!iNNdDtwLuUK?_(SMMyAak~Ao|43;l z`};um!B#_lRx=f}Z$F=tbY$pJ_YpUKv(RbF)|`DnDBM+a7km}|R&nYop0mSpNVrlk z;F#*26-U>WF2kz7ETQnci(-Ww<_z^xJY6Rs_o9mG7^fL#-bpf+;HD6`?t4OV!;8%) zHuIJ;U;9{;*Y$z;yy}thd_IIEz{TqOHZtSt%x8z}>uQAf%D};eThHB%ad(+jf63g& z!U)RV4eO+}*{|a!x1F-sbRR0qYYkqPu1*rAcNa|2bid*eL+&6nqcFNvS4BKQ@9CGj4dEq2WB=Fb8rUOm3u?684ZXwNU&krXgr~KGIx9)>H zF;X*F;A&|V(rZ5~`vOW>@vj(ufD><7Mki&Zp(*V@Ao zliLkECe=s0;il6Nu^1ZnXAWJIw%9EZu)*(bmAGgAQSn8c>;{DJ+$?BtIDwB@?f&zp zH7;5QV1|3GL(xs8AT#@*uIt3rrePl2s1Y+`?ZdVBQOlsz}|LCnh zFkaF0K+XY$qq_)$(+Mol+NJ+wg)&l}`7>G~x!d{gFn`qOUg-iGx4s*GkEFr(7Qt1m z`3HWGB{VwX)}5ff5X=lY@NmETs{88>lp~f*QzCv?1I{TJ-JZ=0$qG)aO%-=LlNCK~ z$arExdY%}HUg3!3CyG)R)+qe2gMZTCSxHcC$r4gQlJpSQ2{8aJw=JwS&T=qLngvC1 z!S7_WqCx+hpwYl;M83z!C7MZpvjBg8a;Zv*fr6T}*#OBp1EDv4#bsJSWL z2%Ssa%gGYU^4q7o_DITFZC$@-u|m-XGfc{S>B1!0JYL2vF-0{5H zx_$5j#|Rnrx8RtdXsVd5lC7z6EWMZka5>lr$q|_fc9?|QF^~0m?^(z*3j0YkWgaVEq(F_$DybLtV2!Y=3mKZ4@8$RcnH zA0oVM>r0HlfeoN_Trw>5AKy*g5!+pOAN!I}BQlXPks(|Uexz0Hp-d-UBvmdl0E#Rk z7wk1PG(fWeJH?GWk?7{~$;a-b>+6-sA>2;F20k`ic)DiPsEivSK1*;g6QGk}6;t-g zbOWO4uc3R2ZL=J+9eyP4NA&*J@gKYomcqB`bExA0l*-^&$Sa3aZFUPrP)=bBD@7{c z=DXe2ZLmz}&s6hrGa?cV_u>aetyWq#v)^OErQIxc*(($37ZHk|S7}8s z&R#Z?1ExEOI_I3<%IuMF*o8^K=P%Bk-^$|0u*PRJ;|WY%qgw;l8TvrG{oqR}HW||s z#$osqr;dnut*h!<-4ZytJqUTvKS+#PJqiHZk0-h^JF&IT=?z`HbbY0Dy3ASiodB=AZkpIKRPR{dZx2Hfjwcp77L1~jb8!LsRF48{5S$IOJ1q`y<~Xp9E34r z{;})FoTH6;6S@V*b!ujzX&tqljKvaRk6tg7+i`9>i!7KP|KM+6N=*P=UAINX^!%se z)JqKbj=uzMvc1FcK26ZyRQNEb8O0-IL-zH>&6^DWTh>yGTdbUH-~8GKN8P|fs_xy6 z#LT*=$9driMRuBDJ(@4wgrfV;Et%n_1B5x>d&qfWw7)bz2jgB;N{vY<9?JXu)b42sW9(>q#D5n?+>h`BLP2+6w19M{RVNA^WaO!8k=&);kZ4@Gm&r z1n`BO^|9Nvs^!f{qaX%{j>F5OE~!U?pKa0v22iROjes%dlyvfRL2=>+a0xXp4!|mr zUQ=7)2mtSP#YM&Z5j$bJdq%*VP5(pO_~;ND{t_-Y^ZMnlkN6v!K;r0?<}$S{v#?H8 zbc^FQR^5vzNHCGq1P}76>M`+UoDp@Up7?F*e6wy^@$DBR_|75UB7Pq5brr*R^C;B7 zEVPzMq3KkK9h0dR^ zn2i`C>>(ZW4J4MNssHi1XdBpf+XT^vm~sUZ4PB`+AB9_Tz*Rkih6!3sWQv2rALK~< zC~T$=+WwK3E@8P+igyGNeA#}$lN1@DCi(E)qBz_0z!*vkjSw5$FZ?^PoOe$AP{tgp zSbmkZV1q&%g{BK!@Fz~lGAj~hQWOiWuY5$l4v;;({50BCB_I5vMcz5!WOb(lXlHPa zI-wg*NUC0l3`yVj?Fu}itY5j|b6@(b06JY*lOim}g156qFP8tx2_5?mQV>c9e%m5P zY`q?DfDj91Gvnd_hk9~9L4}+SjbuVbVmp8YiX%D$MBMQaS*GwAcP(#=+oKMW@q=_B z-2h!*hMfpGHzUt@u9zr<^-~P;azz z#do^d*>{T>hikr%a`j*b_w53W6I}%CAEFv!gp5}47S5+P{}{7HeNR+{`9m!yyRrM- z36G$GG(hCKv4rSv&*Hdv$xj^!BI8W}?e9zSesY%AdQpR9NJ4*iBWAe6%)AQ?2NAr` z7kwnzPske8>R;CW2CzU{c=aAX5EP+uRz@ZGKzHgY@Jl`{a<)pbq6RJaL(3e=!#Gzf zCoaHWT*JMDv#@&CKCS=@e#ojmu~~~az^J{7-v@H9ai!?0G!JA?Pc5-aG^DQSvrfWaT`iV@ zw@!IKKTig1?+Y$xj9~qMei!1v_B7@m0lT0lGmte6rQ%@w1~Pv%LSGask@=U|Uus&v zWmFL>rnXO!vDik`(D3WJ($~DcsQIf%5)Q9kY(I|E;f$8E(c1jb^$^yiid(Hz&pN)Z z!uy(xiRMNKjWrKDaqujkmVV5U!CT;R_ujJ$YUiM+mco(jtNgZ0TX*d{R^B-!#&G8d zAKYcyi4$%doX_Szg&a4&?6Q8*)ozc^?*5ugsxDMUMZf~OiW0nkRuNX*Q4>Zu%ozH&P+)1S1j?`+s@bXDzJ1raZQx_>mL+~O?d@F zmNul0xZ;zk@JmiTKMec*aK|TPrimG4v&Kt=gNdz@xus!&Q)-D>x|;9%F!VWV(ss1m zz&sZr%$OUTjySixy9*?V?ef7)$}(7+agth@X|+jld7w%1(XH^n`2lvkykIl1uro64 zBSfrzT?#_$p1alc!YqAGDd^SoCkTbr%YVwpd5`ubCrbise#Q?JKdkOP17)swCYw$* z);f;~bUW^n`dX`Kz(qfABp>M60uOm6`v=_QZ(V>gk6+i?k9h*)9-BjmnD2=yAIvk% zZ;#eo@8p@WwU&Cf?EXFzV++9;4-AQ1eFRFf>YixPxy_&z{9DXudJxh-jJ%W?$NP3a7)DzB>;22g9caZj!oR019q|RC4;Y4Y-ql+2;pVxK!hE=2 z=p$*`92Vb%8qmG0v&PZl&6?xS<~(%CNTa*Xu+J!yur8TfG*G|zmOmA>>%f$A%rfV2 zgt$6&3-vvd_Dk}W`YL&Sc+?heW6RKEMP*N-q8(B&5eZpJRfalCoM`O-yl8 zs9Wsm4CEtsy8VF38uRP#+LdmcYr5*(aw(2$lS$K*Qe8{eKy_pT|F_IvKygJ*<1F10 zyueH}n#7-^c`K18MOU9Iq34~)BiB*k4Qini=JJvTx+q#0i8!$w zt(SsSOg+%>MH2fS?<;yPg-#V~d{jVi8f^c0N<agNb*xVe@DKE5Q8|4b2vh@v}f*P5BO zJHil^c%aT{4BIC;{8xw}cew2JO`fbFHOL}!UNdH4!ho(`VLzI4RormV$Fk!M_F}Mo zG$JDttYH*&Lc>2O<0wXc|6ssC%&RK%PGtmqQ5yT#epH;9kGRNZks+gB+}oc+Y>>U- zXxEF7{fNRZLyPVb;f$FbdfmT9xDs`|$b{h=TlEeSP$x)Ndthj+r$WyiL;vn`XEy_G z)g89)$(%DzX>3C_A|tHBpd6e;Ih-c88pbZpr$ND6zIylOHT%0zAG`#dWy%Wx-#1!+ z5NwGzKJa@F-zdukIgmd8dtf;@S{GTm*nb1rfVNwf5ow>1i8>)zmLJ7^bXK=}a$Bm# zT3EGs2cP-Stk3_7385%F4!yM51ys{B=#12$eRsiEt_$>rD9@`he6a>`S=+!hPw}rj zusmLU_WL+aB`w8a=X9qvzDiX zUD|L#my~$b3<<2UH}hXHSI}NOL{7#yQxnEdwx8H0D_1kZx4bFv$gYC$;EW1H+4gc{ z8v7k7{^R#?MLHZ)E{SbsN@~?Iloc!LiZNE(q?>nnT_6>>+7)R%r3fy(lv0S&4DhZY zPi~x@SM6tn+E-|y3JG^GmAZ6>vpz4bPvf-eKmSXnV~8H!3&|S*@6T5+qHEP;s7*}5 z)xsoGmcnnu)*RA84w)Pn13POu5HRJ9yF7}G6TJkCjJ-#`()L?&wt+ch9xs<#j=h5# z^5VAo{4!)*<}$eN$=-qId;Q6sMk{O#Q5{v=DoOiE!+w?}(MydxF=PWpQ_uF6Rq(yf z&Ky{&ss@8h=wF&ukRAsRF-L-6Sg&hvx8{egZU@M`p;=P%_&Xk;65f&~%`c3}7M-F! z{Bt$?2w(}g;Me9qjc8M3z+{A3J9_(kutE8U-=sc^l$_gQZoTFdL&?6V@Tw=ge|)=D zbH&rj9aEgZva^HOsjqi=;?j@m%aPAkTrLJz*GedkdL_%TW4WrPS3~7K3#CfSI628( z2HVdAgf^=Y2TV}s%J?f;G!DPqY$MTG*yuR}%OE?Rr`5UlRB-1$Q#av*?C29El4Wm$ zxOX^#Wy>Ga3TI{WH0q+ybq%vF*qHw^;l1VyiFSV~ z?tPmtasPhaekec;f`f#yNKqzGMh2hx@UHl--SSf`6-I{7dE93g&ZYMeuegpJm)bn% z4jrFPQlC>TkKP|qAIOoBM!HeaJrt`(KqeoY_@rB@s_rxt?4@@}U6AyauMjy4$F5DZ zZBd&=3#9LRrVH@-bUcKQ+8!B`L8m=8e7E(hJxlV@*zu6!*b?1PIcb^}@ZSXk!V%>p zjXxJjE*G)o%0KQwcr5as={{|V%c(GY#o9@=T8fChYRXCH!9||OJjc`Ne!?MdvNN#( z_qVZt%o27Q_35Q&3hgHA_$;eqV%1UudGzRseNh#XDf6cC$#uo93hmSUrzZ07th ze$DkB&k=Ja#wLb7R>u4XVJpt8pQr87-^a{%2k(YW5QA$QzGw78Wc6!q6CWA1cq55r z-?oO(J{yIrOh8JSzBGTxYqSR}pa)|j&O(@SvY8+*ihk*7JuDV@!U@7+P7nt9qmEOi z+N5?Jj~?QVbrxsO{0whu{YgDEeXz|`j_6lHF2iqKp&zBwC+-%4>Rrt094Rn^$}^0Z z`e@X&oikx?@2Btadp#{sur7<)rHiky3(|OHy=F8OkeEQjf$IGMcA6v3=k#SZN;(s$g z{CBI=T7HslG9D=#wOn!?;U3XH{`wDraG{H$8WMcCvEAhOfuegASQP3FmdM{+*ng_p1=qwEIMHb5XT@j}Q+wV#`H*rD?EeJMGCa5Q3R2&+f! z+ynSHivi8c5lW8w1)nRs-K5FD$8a)Ia$ia>sJ&rMiZXUOCaFXJ8>A1383oJa$JC~t z+)%#Z`r$8imUBy?Dh`jJul1*JBuNV|i31H5;7#Yrr=SW&9#i|9UwA|H2`M77%nRA4 zaDWxy^_k;6pb=$$zi9;##gon4jPv(vXQn1qf$d_`b?}kVz33NPvVlZ>Tm%;VM!z>luXDD?X$3AGWcJH4Iz019M9(RwJEf}|9d&9q%N`gWD%da z`Avnsq$v1k3UYD1_AJ zKeu46e!>|@*-G7c#Z6h<>TW$yydi{@jPj0o*fpeb|K`+)c#oK%CR|)I4D{Ql9r0WIz8sbbmLxX)P!wsreJ%0nt4B8sYy)1Q&TFm(RdwrFZ|yF8G8j=rA52#2MUkT_SsZ*#u8 zQ)PL^8w=~gtgNW;qKLG%%2+5tt5%ref=(XG@!xj-QV#m6v_fmv4np*{Q1J68^M>S@ znv|q2oEThXsY$d>z3^xH@r~DI&)?yOqaB^dh>kzf#fZ!G&)=d$r2kNfeZpTO ztPO}3QLB$#U~R!gci@osYoqa$@X!bFk3hA)u6u0hdO{G8OjbS|fN#jIKDs(mbdS~o z7|VNXFkudc3rdsx=Ea{h?_vKV6BpKCc*ZMUlZc*BkHfb&OLje`{MsfrRE2KU!u*Ys z%C$~{nQqy~2z{uR4Yg<-Z=zWj8o^4aCv~^?kA7Y<51r)VLsh(U*9XqB8@W^jw%-Nz z$lecG_ZzE-tywE9w~I7MrA>>r$Gmy5eT>6^i3_xnE$ee(Cu{dH_aCNFui zz$0yPO1PdZA7pE7GCX76BPo##~HA`>X9l{(fAo0j8AL6I{pMp-(bwO$8oDqhu- zy&6`A!ntsuL70(?7^ULX|EFuQ74*o^tWu-IPB&hb;(ZtqY377aYgN^A8vdH}>SsVd z7g9wn-g#u0Hlydxx+%F_wCer}X4?C;in!_j7~cB8P*QV}j(51#2sXPlz@6*BTy_aT zU%6>azEbwcBFFu{rsTVGRb8gHdLvTWaiw~F@-8Ux$maJ=JI0P)xV%QK2Jj3u!y5B#d6sqlqSS?>M8`H_JWZ_n0Sl z^zWr~efNrN;xQf=HCKBJ1LF^qA1Hqm_XOVVCjEFu^2|O?JHPNIHhok(4eIoFU~TK8 zLfKsZjYDW=P25<^RyP^!(k!-YU-Y|?_hua9*LpChG+=DX9AtC8{(iDgA^kj2E@e{w z=7l0jAM)H32>rIpR)=p-wxDrosPg2Fy0g!)T1KUeBF;dZXW$h5xcH71fh zy^?&1*lea#IzUohiW-0hLW5%+dw8L#ItQzv0j1^(*B4^#@o5US3GLaAwL4%YuGn&eNx!gB`t#c+Dvj{tP)oT>d z@_BVR5gJ|=9HoMgSU(&_gf!U;_cjfNQ$On>@9GS5gK-3llRrfrZJ_lnO2fXYmRN)RH#(`>e`_A;qUct(CsrSNTARa!ece?6z45qnr zVt<;2NcE#%25r0cUgDXVY`!zGp34hu;b`Fczlew74-wrp6Kr-^&>Pi{Iy(${g?0ca z<+&yQ?00*Bzjvx%BbjsJO5|?ElL!2_W7pNPAv#xQk0Afr}4~I%CMce)pPxz zyYEWGtRGBMzcyb?T@FIBY6(!POmLR12r#}h#EkO<*N92F0Q?Qx&X^x9zW!=t4mj%! z6Pbzd6a-u>c4wxVzzV<)ZXb|JYEaMfM3xa2FI|x@j&^Yi>jK<~?Jv6Gn-|!scG&>G z9V~+6A#wCRjR=`%d-L+>>EndEm1i&*M-KLYZ@099Y6V3%VsNuEmR@DKKp1C;8EaR- z;L=Y#&2}3xIiUqtLoV1SE01bUTNU%N1T>X{1C?^q*76=(Wq#MbC|qZyopg>0`cnB6 z^CeQPz)tm1&5=su$5$hM$Ggrc9-e3e7U&!#G_q75#7Ej6Ue982#UyeO5DbnrIhv}E zTN*ynH}_u6o5G-!Wt7cm<9Avag%80X+nsOs(fBuXYCA4Btua$xw2%4szV`E?Z>Dv( zSSyZVJy2t*D&Bnqrg#-m26Ps|*K)>+a?Zg0-0`yo8HSG8?f$9CVK^xbY__p7|`3R04F zUL1XTp2eS&)TBy3s}BoFs<&&i1inYvJoNI!8C0>4<_57rJ5}r{0(b+K(uGmyaI$eb z(L+U)Os<*@U`UB@uLhJ#YO~?sZm@sVw4dg{qT^_PmdFlfAUNrVmkZrYa!UBW zt)`KT2B`XM4;RWD)(8s^@XiAanRH1EpSeJ6a{R>7{~1(m7HJg678Yx02%AjplkM3U zA-t*hPYnNyEaXVp2<_Y@a%K5O>g$q!tv$groR<%q=8nYx-j9-Wo8IgT8pvZ%$cL)y zqECFKe+z{rXQRSPkiBBj$aR2&D{zc|X{^y0l$bHwR^9 z9W@J5yv_gPQx2w)qI|wZP5(|V*_L=SW9*etSxG9&=J)wNo)=*BWA8p;1h|S871Lsu zhBXTzl{L4SoW@5*JE@;M`#|Sgph%}*^+<8vdH%60X>dXd`p|3+t_70CsIke0KKToF}X{1fX124DL*6P zMEaMw2ybTG>@s*>KAck>UtxK1tm+~mSaRaBII`Po*YT4{Yz2XZTa>d98^sO#m_)14 z;j&J=vHewSl3z0yV^N%=K6OS71*!yeO0^RkExb{hNU-Jo1 zZce@&SGEmUq zhqV=Oops|_AT0@C$Z|`g|5{CGc||SyOSLPz&q3J@&C-Q&6`J0y>M;tYC!%Qd?XqZ3 zK))6i*DwRly(8@6ojy-FN0Tk*;?v}k#%l~b`PUEY&*51xu7uty@?oJ7LnK$qH>Mt9^PqKnfcE_j0kAcI&mFKXa8R^{9i4-&j(bJ7z&89P)ao4h&M zu^kv^yC(Whtbm_IJ?auD-@Y3i;|xyB*{)n}n%I}^)rR~qxFD5W8To1G{IUe45x6xY6a1Ks`y`*Mw{IL8=%`0QyNjT;q4BQ3Qa)75f77AOZ@v zKWTQ#bG~oQ`R~v})A$vFVnZ>hxn$w4& z^HO|osYda73nuY9uH-0>HpV8D)~@m+kCr;9n8#kUsiek}x6zD*F0}!?wbulpPIrhz zjr9*mQHosg)wfoCe_F8h+KJJF&z&Ma8gH|-(bC`jGiZ%15=hqChyf(e$a_QWpdFzX zQK`11;Q_zB%@RMIxg|tkO|d2aZ|jE-U|N!>-W`mRMb6}n!I4SXQ>h&We{>O+;Pzv} zQR}%Kft)aY2ckleq_Hpm$S3opiJ6C|`rea{UHVIas(wYPZ1Z*m&J+kOD9~dIEPQ(R zDF%tP`;v0k2K$u5Zh z*ywBk{vyG4fmjHo6y})JV$CLq@R?!yWWN`ux(k3!s;2G?`lkk)c>37qSdUQRKmil; zM{i&GpBw7(u%1uw5Fu?p3g>-aKi#BFlh6JJqScOapA`<<#^W=OTTZv$$Q2bI>doZ` zD537R%~jUD^#~QmM;Vax1D0wVgP;u+fe*Y*R1?YPkSr#7if_P*k zFm-4IMBr6j$a)Q_)NtkBIF@m?f`&_zQd`R13vH#r+Yp9GhcjyY+8zVGC+xvv%2)ch zk9euIPT@#AVO+{URTg`J?@*)jGOjhdj~F7FDQHTS7q<1h5AhP4s|H*}e{;C7+*81> z-(0CQ+^xl?P{#wUE!(*f8B52%7V@;DxqE!Duo{sfooz1CgoSR8E`+6YWp2;r`H?Qle?7*VEQnAwezT-xO9MmLrZxlM+j*l zCzn0~Rt3#%@m)_SssJZZslKiTIucvnQziLNN{do#ni=z7@Zv$$-Z#>is=8BhB7g7n zHM~faX($=;l%<5(JyA6Ed1zwh-2m#i{?4y5jSN;{%Kkb zX2NXF7^JNYG|RXk;EW$1XDL!c)AHFUEwW?xj-4=1O8q^+*)-^4e~MAplS zxcL+>4@Rmj-e~Gljadeh_xtV714M>F3bjG6WSKAO2=?*x(>|SjHM1zBIxOTdJ$H_+ zv}%zMDm!T~OrrdoQFw%8`9_S&UY=TGc1Vsim^6Fu>-6G4;inYA@*F^DXJDOJ^l>Ny z)86%jD>giZ}y=H zNk>z1Jbw&7{EPd1OK2<$^c=Aaf@D%w;Z0k1JqaG-7TMQP7ZZq8t8T1S!&2Kow?Kt^HXc!cPsb(}RI6U2tAwVY4tRU|kM_ze~d_)2gl8l{eWovo2x)A`g zYw}CA;Q8qT^|NiEhnmGZ3Vx*zC!S;Sy0={;+3Nml723aN;d=EsHd{m{D;LwAf$#@+ z`@X6bR^ujgBniAmYo`cpG45J@z)R)iS#HBjl`2`k9h4Cum-t(n8LjW{!W3q3%VHQ# zX4hAZ(2P^+P6chrfHU$UI0Gtc5v7M_Y|&hyHeR^9E_GtDoJ-0x+DvcK&l>(Hn=N`; zG;`&43{VP8B>)PZ6}Ml5&V}OZDYf&#j^GRngwqMe`N|DTNxSUL#(v5lNPHyiNr^CWzHpn^&z46l!jYC4t-JHdipS6B>P?xpT!Fhu3bY z_+tn9X(0-&>T}tFc3s5${P9^Fg&mb37Aem^Z)jSz6f592!oLcu0AWGLN@EO6R<4)* zM)(BuR0R?OSY;KwFZpT-`BLpBr7`QS{T&ZQJ@G$Dp_AtR`Y9hrA0{6B`XW&<*emg* z(Db;6tyCg=^7Lksu=>)8IDdcKcJdnsW$}Se!N#p_0d0>Pv~DHZc|++tNq5rJ#4|!V z#?&5Q%O-yW|1fZhONUfGkz(Vwhh1akYqOIme3^OfUtVl`#LkyXgH;luLTMkz9XC&n z6!#pnAg3Po1{w}2%G^s0yPTd8xku?)rk_h+rlWeZWu00@hb@@<*hv;_S5RlvI2?V6 zS8e!udEfcRZPEucqP^Qh$?CUKY)kHA5;GW&g=PrFYA^s}$M&49gk^AC*{0~^3IPcE z#LjbairqjPBezR49&J)1Bqm>sT)c5`$j>7?%#G?g4#bYW3G`AI`9uEKoj>COn$&@$ zjB};if)eWwnGY6&)^GnZHX|89qOD#&P)%k{eX_nzRg3o16HTbecn%jQBqjq$9|9Sx*_(an*?T%rPiCdA!0R0vjA4M>oE(BJ!=3D+LU7AOYQ& zuK^pTxc_#20tBuez1!_QI>E_NQ!}n>gBoyQpaiR$%uO!g&8}Eq1VSy*axudM7{wv< z7zaIQ2a{-s zyAu;ggJAo8>R1bdSgl+UvHtB8z4)iadYrfw?Sten&vTo%Sh?ZS96PFT<~}vfovn8! zQ2pree#B0LnkNXx8&j^VhGd^(sbv%*c*}(gy}-4eV{L(a$Kpz}_k_OHNqfCUPsykV z9A;4@`L>v)AaFMjyS!c}HdXgX{R zWp_`t;EJsK1Jrv{`r*-rDeq^BKEaZgJ5lGHfkq%^jc>QUSN2?cHNt@n#5yF#yfL&m zL-=Hlq?8dY0eyz#G`%FOF(0Z}GB<-hVs{^y%77{3$M&u)8hjE32yB{|2QA9wf z-h4HM-IMV75Y_@N+e+!LO@r$5;8%cYG=-krO5b}C#}Uv<94s!Vs8QaEnqZ$UqEyf8 zUtqwphb|x)wSyUBIcfjj&?G8Km>LWIg&kA5b!xwxgXHXZ4k}`eru$}8w3bzq|N&hN2lF`YUJSK?>-wu6+;FS82*KP!Sav>_> zDW28*N%&I?9y-Bx>=@*o5-(~y|A1i(Mwq_DZb3!X#w%QgQj8QZ8oXAz`BoE~ld7a; zxN!mI#pR=!(~RUG3L`Xbj1Rz$Uxq{*p{fYzsQ$%MafI2(5Dw;Qo< zHQ>bCb%Xxub53ws#aw+Svt{8N-2I4PBik4?=Z$^sN@|!cSg)PyYXYM0Lrc#f+bAGW z96o%}z9qO%{6T7E?YmMR>9q4_<>r@h4rk*PrOF1%>tIFT8yr7W_&9l{G+=BY<$byG zG$Q5%_%2NVim+3QWx<*vxIjAI5Hm5+hy(f{JSy*xnB{^Qduw&YhekMygh#qn$s@*} zQK8(cKS2t}KSn@uwCubz$k`UUo;5JM3aSrdyaSZxY5bfn|Bu*=&)^C^%O(=BA=X$9#yTp`kcvj`lSA+#Y}v`|KI6WSz@EZ#2>~SfGn%pUL5Nd_BwUHDK?p z05dY?r)z2_tGEMbU8OCCz+2vlLE~UF#>n3&JNp`hoySJ3z88m+uX4@zLdh{BbNf~eN>VKvJXv{3Z+CP!}%mZS}{*MBoVTDWO*+IDGI{a2Wg$;PKmJDaM0 z|M1G=K^5Eiv`?CIFM7Kgc+x&H&oUr)6O@t2A0vqmjVqwk(4%r|$u?$!&y$i5D&!I_!$UwQ1 ze~qEn!#WkzPI+y>tpohfx5GG%fY5vg!aDewV^-Q~5g=QD5F8IR4Dnbi>ycI2|2>^d zH@>MfRxC5RZ&M5u@*Kk_ad6K34(d%+l4pX5Nj7;;;iKXydLt${LEE5dz1JB0OY7@D zy(Kx<8P+Z$jo@G^XoDi-mXDCIftoi|SrrlbIeC&(ipf_2l|pTNeF3Q&aWXqu*wqM_ zuOus$Rh8!bLJK>rbs8qUa7rg{@Kc{-hD3stnVzr4f`@v(6yG+Qo1}o^g)6B|p>YGM z#JON^EKzJyratzyo?O5XOO_?RP!o005K58f%gn|LjU5`E4?>0p!>{$HWJhk(t74_N zYgLX0XJYB~fn3o~(m@}K36n1oVFYD^=xJ4Y&+z1$o^K`QnKELFcyay7<4P!=o7_NF z#ahizUALX5f_2cxTGb^g{wqD;)lMgFf(87zr};jHx=uAYWdCGPO3j7)PPD-FPx{#u zn|eWI<|p=)Ucba*8s&+JnA<6xx=%JaBnPQgn0-5^b}*mwtY_AHHtK|Is=T-iL}Mdw zX6NMJFHL6clW^hY(iCFpT|AkjryN3LU$B!=xN%rCZqfbag#IylK2SYbmd-T0@ICPF z$qck40;h1Nn^F;<L;W$lpm*`)BMIj4y=|Gm!Rrk-#$`IHJQHlm9nf61{o`}>Qi+E*8npqj0D_l z#i#Sfmf!0r30DpuHnl;Evm*KoDn6z8Lu~74ztjO_kH;cg$((3hH02?r$Unh6x?iSG z!)kd{02Nd9Lb@8oe)@)iDw>rY_lYhnjrVN=2b_IT16rhd02XB7uVoeA12eLNVHp_{1t~l@1S~?1I}T47Q18PjBHYg_#|>ZV8Sjv?saukd_7ZNv z1(`Hd-7vWG5(mV3e@2m69CgS}6mJ>Bw&5Aqu{@D+?TIHlAW-%hr3vg;ILvk0$s_gAHpc@M~`n&#di-58v@U z*8l~VSD7ZjWVl^EytlM2zs`?2Qtn+5(BM4`nd8!&mmi>3ARn@4Ss!SK7%V59r#=@l zMwZi?LhXDj>G>uGQlBPqcX!g&bdT~C(^1Q^WZ&hr?f9yLec!=Z9G%=AO@AJ^J81+R(S=hgcFzb|xC8$8a_(W0BzlP;8PJyw6C*ebvu39gmd^2+RX-@vd0gJqe%; z$-fIJWt@E@)Bk|0^DJWiqm=zlUM zn3mz;87v@&wWW~hgJ_(-NTKjyrgfSMEnLM3mfPM9Sz|2Z7QptQVgl4SnW+@@I3>Q^ zv;rz7f22lK5y_@P9|lZejUtQaMZTx(rGI`!4t@5NTNaaomqXqD3guif`e_L3$`yIm zRwQ_2Rgk40(C>6%T#fe$=@Zr5-{zFVnkzQF&VWb5HEpV7^OR)23#E>IJr#pxS*kYA zfd5YXNU&D_JqPan-6U=yNyM%l<{MFm$wSbY$3S;mNFc(#brRq zGZ@xAl})+pfJ2lx6egUIv$pi6iK8rZ^d3^2t=X*g*-(U=r;F20gnK4c_{ox{r>1YW zsJQyKKNu!_cuAikF8=CNuxdmpq{BMnDl?j9WBsLg0mw#!$;+H6_pWj&=s$CW@iRD& zNf-o)egUMQ-en(f@?ey>k5m0rd%mSD{)f%J0m9=4&iZ2oW2Rc9iZ2$lO literal 0 HcmV?d00001 diff --git a/public/logos/gate.png b/public/logos/gate.png new file mode 100644 index 0000000000000000000000000000000000000000..9e5900f4640f000794b01c201d49d3733c0b55e5 GIT binary patch literal 16203 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 F=zo~QqT&Dm literal 0 HcmV?d00001 diff --git a/public/logos/kucoin.svg b/public/logos/kucoin.svg new file mode 100644 index 000000000..68462f47c --- /dev/null +++ b/public/logos/kucoin.svg @@ -0,0 +1 @@ +KuCoin \ No newline at end of file diff --git a/public/logos/mexc.png b/public/logos/mexc.png new file mode 100644 index 0000000000000000000000000000000000000000..3f783c7e268bd0661facce19065ea7b5d07594cf GIT binary patch literal 12812 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 LMXFN5*#Cb4hZK6O literal 0 HcmV?d00001 diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index a62b8078a..25cc72142 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -187,6 +187,16 @@ const RAW: Record = { paradex: "/logos/paradex.svg", aster: "/logos/aster.svg", + // ─── Perp funding stability cohort additions (bench № 043) ─── + // Sourced via Mobula CEFI funding-rate aggregator. + bitget: "/logos/bitget.png", + coinbase: "/logos/coinbase.svg", + deribit: "/logos/deribit.png", + gate: "/logos/gate.png", + kraken: "/logos/kraken.png", + kucoin: "/logos/kucoin.svg", + mexc: "/logos/mexc.png", + // ─── Hyperliquid HIP-3 deployers (bench № 035) ─── xyz: "/logos/xyz.png", vntl: "/logos/vntl.png", From 2bdcd5769c1a81202465aaa9a328c94944c475d5 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Fri, 26 Jun 2026 15:05:13 +0200 Subject: [PATCH 04/60] fix(perp-funding-stability): trim methodology[3] under 500 chars --- benchmarks/perp-funding-stability.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/perp-funding-stability.yml b/benchmarks/perp-funding-stability.yml index 10f92e7ed..ee15d5fb9 100644 --- a/benchmarks/perp-funding-stability.yml +++ b/benchmarks/perp-funding-stability.yml @@ -44,7 +44,7 @@ methodology: - "Source. Same upstream as perp-funding: each venue's public funding endpoint polled every 60 seconds, normalized to bps per hour and per 24h hold, sign preserved." - "Metric. stddev_over_time(perp_funding_hold_24h_bps{asset=\"ETH\"}[24h]) per venue. The 7d window smooths intraday noise but reacts to regime shifts within a week." - "Sign handling. The underlying series is signed (positive means longs pay). Stability measures the swing, so the ranking uses the unsigned stddev. Average direction and level are the perp-funding bench." - - "Cohort. 15 venues across two upstream sources. 7 polled directly by the harness on each venue's public funding endpoint (perp_funding_hold_24h_bps): Hyperliquid (onchain), Bybit, dYdX v4 (Cosmos appchain), Binance, OKX, Paradex (Starknet L2), Aster (BNB Chain). 8 sourced via Mobula's CEFI funding-rate aggregator (perp_venue_funding_24h_bps): Bitget, Coinbase, Deribit, Gate, Kraken, KuCoin, Lighter, MEXC. Both pipelines normalize to bps per 24h hold with sign preserved, so the unsigned 24h stddev is directly comparable across the full cohort." + - "Cohort. 15 venues across two sources. 7 polled directly (perp_funding_hold_24h_bps): Hyperliquid, Bybit, dYdX v4, Binance, OKX, Paradex, Aster. 8 via Mobula CEFI aggregator (perp_venue_funding_24h_bps): Bitget, Coinbase, Deribit, Gate, Kraken, KuCoin, Lighter, MEXC. Both normalize to bps per 24h hold, sign preserved, so the unsigned stddev compares cleanly across the full cohort." - "Success. Same freshness signal as perp-funding: perp_funding_venue_ok_unix advancing within the last 5 minutes. A venue whose scrape stalls is tagged stale on the row." - "Cadence. The upstream funding harness polls every 60 seconds; Prometheus scrapes every 30 seconds. The 7 day stddev refreshes on every scrape." - "Failures. A venue whose upstream funding feed errors keeps its last gauge value; the leaderboard tags the row stale once the success gauge drops to 0." From ae6a26e40956d968817258d6f3b4dc32a5e4a06b Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Fri, 26 Jun 2026 15:08:44 +0200 Subject: [PATCH 05/60] fix(types): re-add venue dimension to BenchmarkFilters + spec schema Dev was force-pushed after PR #730 and the venue support in BenchmarkFilters (materialize/filters.ts) was lost in the spec/snapshot refactor PR #739. The worker still references f.venue and dims.venue, so typecheck failed. Re-add venue across: - BenchmarkFilters union (load.ts) - parseFilterSig allowlist - Spec.dimensions schema (spec-schema.ts) so YAML venue dim parses --- src/lib/materialize/load.ts | 5 +++-- src/lib/spec-schema.ts | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/lib/materialize/load.ts b/src/lib/materialize/load.ts index dd77a6b49..370e4b974 100644 --- a/src/lib/materialize/load.ts +++ b/src/lib/materialize/load.ts @@ -42,6 +42,7 @@ export type BenchmarkFilters = { chain?: string; region?: string; kind?: string; + venue?: string; }; export function filterSig(f: BenchmarkFilters): string { @@ -58,8 +59,8 @@ export function parseFilterSig(sig: string): BenchmarkFilters { if (!sig) return out; for (const kv of sig.split("&")) { const [k, v] = kv.split("="); - if (k && v && (k === "chain" || k === "region" || k === "kind")) { - out[k as "chain" | "region" | "kind"] = v; + if (k && v && (k === "chain" || k === "region" || k === "kind" || k === "venue")) { + out[k as "chain" | "region" | "kind" | "venue"] = v; } } return out; diff --git a/src/lib/spec-schema.ts b/src/lib/spec-schema.ts index ab57dac61..d57e8694e 100644 --- a/src/lib/spec-schema.ts +++ b/src/lib/spec-schema.ts @@ -344,6 +344,14 @@ export const SpecSchema = z }) ) .optional(), + venue: z + .array( + z.object({ + value: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/), + label: z.string().min(1).max(64), + }) + ) + .optional(), }) .optional(), From 6d49897443837eafd1798a31c8da3901d9fad5dd Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Fri, 26 Jun 2026 15:24:27 +0200 Subject: [PATCH 06/60] fix(logos): coinbase + kucoin colored PNG (was monochrome SVG, invisible in dark mode) simpleicons SVGs are monochrome (renders black) so coinbase and kucoin logos vanished in dark mode. Replace with the colored 200x200 PNGs from the CoinMarketCap exchange CDN, same source as bitget/deribit/gate/kraken/mexc. --- public/logos/coinbase.png | Bin 0 -> 2027 bytes public/logos/coinbase.svg | 1 - public/logos/kucoin.png | Bin 0 -> 3832 bytes public/logos/kucoin.svg | 1 - src/lib/logo-manifest.ts | 4 ++-- 5 files changed, 2 insertions(+), 4 deletions(-) create mode 100644 public/logos/coinbase.png delete mode 100644 public/logos/coinbase.svg create mode 100644 public/logos/kucoin.png delete mode 100644 public/logos/kucoin.svg diff --git a/public/logos/coinbase.png b/public/logos/coinbase.png new file mode 100644 index 0000000000000000000000000000000000000000..9c7f8393e1184f437ec27e856f799e0e33e15d50 GIT binary patch literal 2027 zcmbtV=~ohn9wjwY7NJ})bHOywQj|o)1s7DbW!%bb+#M^Lv@(})z{=eMwX$r)To5Kp z3pL3SwcN{)g31&$XI#TwGtI)r>AiQ}Uoao;{oQlF-E+_R-OD~+9#D`D2mk;;4|uxy z{kSjw0~O#8eh|bh0|1mC9&mFxN(rAY9ZGrU2`A|(7{b$CkxD|!mk)+`#oO)I(|Q5> zEqKyf+&PINJ1V}_tLJQVdN{hd#BRx&lj#90Ka?;Ko@r; z`Qg~0C1rCpT9UfHb(p2Xk@ep-6w8u9i`IF6LCeuYk5Wav^rSalJ&(?0s)-$>bOCt!P0kX3}>@@ro81`Yh0R*PJPh_Sm8&GZpe_e zG^k4}`LfOS`ai;0d9JZ>8#m4)mbjsF{TLC+a4qHt51ZR6`RsFOXT~7=ClLv`c50Hf zT(b1l*$6Iww1dNLLaCY-6mT^*8$Z=A zY@y zT-sY`1T`?tntE5*0JNMrT$brT4kx5&mA2EbB80UF@3Ew#=$wGBVqNO8v0Xia1!0~r zD0|{m)l;&XqKKNJAK&^|5I?vloZjW>_NXuM%QiJwJ2Z)rV z76D0`hc&)A%e$*BVA5%W`EOxzRh?}SA7|QcS&ZGuI)21U#8Ua09I(|SU5n|zR1bn#9D0*}`mKo~OL0`f*wMsAUa;7jgev2WrFfLfxLvX=`-< zi^>Tl8y(nFhOFQ_yeXL%s$EX)A3Lytq8%F3?vIvTAq{lgN|IRm(MOl{JZX(M9{YQ)68sN%IrV(LeJW#CERxDdTKK=iYb zQ(xa3#;HVKwCP-|IVh$J#X!Tg=>1=+cu~28L1<`t*T|e&95Q-uvn|h$$I3OIE5buU zchZC2E=0BIHooagu#=wcdXr2YiHAS|^&FHK2|q+G!gE40T>9>%Jh`0vM`8vr`^o#= zu-nDXif%2#RC5ahPTDTa+wWB^){$LJ%;tf3Qyu)6ZH@%LMpl3q*bev9d#I<~E!)uu)2u)e8#=_p<_-W8ruOSg2v2b7A%`DqztBDd)D*$kt4Ng>3oxM^}bdY)vWN>#Tfy7H{*48~{0kD-}m5*3cZ4=4eoS??>d@>_kn9~rF5cb zURPpp<4%xE4Y8eJeh%T2#gL#pt($SrPEi0UlI(J2P_6ek%g8z5bjJV(KTE9@D#*$y qpPHLh>ZW$5JmZc5+iYE}|5kobzDr7MvD^GH;(!D0UT$1sWcojNCoinbase \ No newline at end of file diff --git a/public/logos/kucoin.png b/public/logos/kucoin.png new file mode 100644 index 0000000000000000000000000000000000000000..41156cba2e196f283ce0d63bbd9668a479ff749b GIT binary patch literal 3832 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 literal 0 HcmV?d00001 diff --git a/public/logos/kucoin.svg b/public/logos/kucoin.svg deleted file mode 100644 index 68462f47c..000000000 --- a/public/logos/kucoin.svg +++ /dev/null @@ -1 +0,0 @@ -KuCoin \ No newline at end of file diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index 25cc72142..1f383257a 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -190,11 +190,11 @@ const RAW: Record = { // ─── Perp funding stability cohort additions (bench № 043) ─── // Sourced via Mobula CEFI funding-rate aggregator. bitget: "/logos/bitget.png", - coinbase: "/logos/coinbase.svg", + coinbase: "/logos/coinbase.png", deribit: "/logos/deribit.png", gate: "/logos/gate.png", kraken: "/logos/kraken.png", - kucoin: "/logos/kucoin.svg", + kucoin: "/logos/kucoin.png", mexc: "/logos/mexc.png", // ─── Hyperliquid HIP-3 deployers (bench № 035) ─── From 5cdf646fcb942fc996a9c2a0d6d5d423e5f38de1 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:31:16 +0300 Subject: [PATCH 07/60] fix(main): restore src/lib/categories.ts + binance.svg mapping (#752) PR #743 created src/app/benchmarks/category/[cat]/page.tsx + benchmark-grid.tsx referencing @/lib/categories but the helper module was not cherry-picked to main, breaking typecheck. Also re-apply binance: /logos/binance.svg (was reverted by auto-merge against pre-existing main entry pointing to /logos/bnb.png). --- src/lib/categories.ts | 110 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/lib/categories.ts diff --git a/src/lib/categories.ts b/src/lib/categories.ts new file mode 100644 index 000000000..b1eaa9885 --- /dev/null +++ b/src/lib/categories.ts @@ -0,0 +1,110 @@ +/** + * Category taxonomy for benchmark hubs. + * + * Mirrors the closed `Category` enum from `spec-schema.ts` (the source + * of truth that every YAML spec gets validated against). Centralised + * here so the `/benchmarks/category/` routes, the sitemap, the + * client filter and the meta-builders all share one slug ↔ label ↔ + * description vocabulary. If a new category is added to the schema, + * adding it here is the only other touch-point needed to surface a + * dedicated hub URL. + * + * Slugs are lowercase, kebab-case, ASCII-only so they survive URL + * canonicalisation, RSS readers and JSON-LD @id fragments without + * needing encoding. Labels match the schema enum verbatim so the + * client filter can compare `b.category === entry.label` without an + * extra lookup. + * + * Descriptions are SEO copy that explains the category scope. They + * land in , OG description, and the H1 sub-paragraph + * on the category hub page, so each must read as a standalone sentence + * (a) without the surrounding template and (b) with the category name + * substituted in. No em / en dashes per the project copy rules. + */ + +export type Category = + | "Aggregators" + | "Bridges" + | "Blockchains" + | "Trading" + | "Wallets" + | "RPCs" + | "NFT APIs"; + +export type CategoryEntry = { + /** URL slug. Lowercase, kebab-case, ASCII. */ + slug: string; + /** Schema enum label, matches `Benchmark.category` exactly. */ + label: Category; + /** Plural human heading used in H1 + breadcrumb. */ + heading: string; + /** SEO copy under the H1 + . */ + description: string; +}; + +export const CATEGORIES: readonly CategoryEntry[] = [ + { + slug: "aggregators", + label: "Aggregators", + heading: "Aggregator benchmarks", + description: + "Live OpenChainBench measurements for crypto data aggregators. Compare quote latency, market coverage, and freshness across the providers that fan out queries to multiple underlying sources.", + }, + { + slug: "bridges", + label: "Bridges", + heading: "Bridge benchmarks", + description: + "Open, reproducible benchmarks for cross-chain bridges and intent layers. Compare quote latency, end-to-end fees, and route coverage across the bridges that move value between chains.", + }, + { + slug: "blockchains", + label: "Blockchains", + heading: "Blockchain benchmarks", + description: + "Network-level measurements that compare blockchains head to head. Time to finality, block time, gas estimation accuracy, and validator economics on every L1 and L2 OpenChainBench tracks.", + }, + { + slug: "trading", + label: "Trading", + heading: "Trading benchmarks", + description: + "Latency, fee, and freshness benchmarks for trading venues and execution providers. Perpetuals, spot DEX quotes, oracle deviation, and Polymarket data freshness measured continuously.", + }, + { + slug: "wallets", + label: "Wallets", + heading: "Wallet benchmarks", + description: + "Benchmarks that compare wallet infrastructure providers on label coverage, address resolution, and the data layers that power production wallet UIs.", + }, + { + slug: "rpcs", + label: "RPCs", + heading: "RPC benchmarks", + description: + "Live OpenChainBench measurements for RPC and node providers. Compare capability coverage, transaction landing latency, and reliability across the endpoints that power production dApps.", + }, + { + slug: "nft-apis", + label: "NFT APIs", + heading: "NFT API benchmarks", + description: + "Benchmarks that compare NFT data API providers on collection metadata coverage, freshness, and the indexing depth that production NFT marketplaces and wallets depend on.", + }, +]; + +export const CATEGORY_BY_SLUG: ReadonlyMap = new Map( + CATEGORIES.map((c) => [c.slug, c]), +); + +export const CATEGORY_SLUG_BY_LABEL: ReadonlyMap = new Map( + CATEGORIES.map((c) => [c.label, c.slug]), +); + +/** Slug used in the `/benchmarks/category/` URL for a given + * `Benchmark.category` value. Returns `null` for unknown labels so + * call sites can degrade to the unscoped hub instead of a 404 link. */ +export function categorySlugFromLabel(label: string): string | null { + return CATEGORY_SLUG_BY_LABEL.get(label as Category) ?? null; +} From 1028641a012d1506cb5ed55076063ff321c53b27 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:34:30 +0300 Subject: [PATCH 08/60] fix(seo): blacklist hex builder + shared composite filter (#747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(sample-health): wire UI badge + API surface + insufficient silencing (follow-up A) (#741) Completes PR #732 (sample-health system) by surfacing the computed dataConfidence/sampleHealth/expectedN to users + machine-readable consumers. Was dead-code on the data layer until this PR. UI (ledger-table.tsx): - Filter rows tagged 'insufficient' out of the sorted ranking. - 'Low sample' pill on rows with dataConfidence='low' + tooltip showing the actual sample-health ratio ('45% of expected'). APIs: - /api/citable: returns expectedN + dataConfidence per row. When bench-wide aggregate is 'insufficient', returns value=null + leader=null (refuses to crown a leader from undersized samples). - /api/stat/[slug]: same treatment + drops 'insufficient' rows from the machine-readable ranking array. Per-row sampleHealth + dataConfidence exposed for downstream tooling. Library (citation.ts): - citationCandidates() drops 'insufficient' rows from the leader candidate set. - fieldValue/leader/headlineSentence return null/'insufficient data' when bench-wide is insufficient. - isInsufficient() helper preserved from main (used by hub cards + /api/citable hot path). Effect: bench pages where YAML declares expected_n now visually communicate sample confidence; agents/journalists hitting the citable API can't accidentally quote a leader drawn from 3 data points; the methodology page's 'n >= 1000' claim becomes honest. Co-authored-by: Florent Tapponnier * feat(seo): /benchmarks/category/[cat] hub pages + BenchmarkGrid lockedCategory (D.2) (#743) Final cherry-pick from dev-archive — adds 7 new SEO-indexable hub pages (one per benchmark category) that target long-tail queries like 'best crypto aggregator benchmarks 2026'. D.2 was deferred from PR D because the BenchmarkCard ↔ MiniChart ↔ BenchmarkCardData type chain had cascading TS errors. Follow-up A (citation.ts isInsufficient → InsufficientCheckInput structural subset) silently fixed those, so this lands cleanly now. Files: - src/app/benchmarks/category/[cat]/page.tsx (new) — server route that renders BenchmarkGrid pre-filtered to one category + breadcrumb + ItemList JSON-LD. generateStaticParams builds the 7 hub routes from CATEGORIES. Each page is fully indexable (canonical, OG, etc.). - src/components/benchmark-grid.tsx (updated) — adds two optional props (lockedCategory, allCategories). Backward compatible: when unset the grid behaves exactly as before (used on /benchmarks). When set (category hub routes), the grid pre-filters and shows the full category pill list so users can hop between hubs. URLs that ship after deploy: - /benchmarks/category/aggregators - /benchmarks/category/bridges - /benchmarks/category/blockchains - /benchmarks/category/trading - /benchmarks/category/wallets - /benchmarks/category/rpcs - /benchmarks/category/nft-apis Each renders the canonical bench grid filtered to that category with its own H1, meta description, and structured data. Sitemap pickup automatic on next regen. Co-authored-by: Florent Tapponnier * fix(seo): blacklist hex builder slugs and shared composite filter Drop 15 anonymous 0x... HL builder slugs and 5 dead composite slugs from /products listing, sitemap and individual /products/ routes. DEAD_COMPOSITE_SLUGS now lives in providers.ts as the single source of truth; related-providers.ts imports it. Bumps providers cache key v3->v4. --------- Co-authored-by: Florent Tapponnier --- src/app/api/citable/route.ts | 32 ++-- src/app/api/stat/[slug]/route.ts | 95 ++++------- src/app/benchmarks/category/[cat]/page.tsx | 112 +++++++++++++ src/app/hyperliquid/page.tsx | 12 +- src/components/benchmark-grid.tsx | 89 +++++++--- src/components/ledger-table.tsx | 85 +++++++++- src/lib/citation.ts | 183 ++++++++++++--------- src/lib/providers.ts | 48 +++++- src/lib/related-providers.ts | 22 +-- 9 files changed, 474 insertions(+), 204 deletions(-) create mode 100644 src/app/benchmarks/category/[cat]/page.tsx diff --git a/src/app/api/citable/route.ts b/src/app/api/citable/route.ts index 03d6b55ca..e1283e1e9 100644 --- a/src/app/api/citable/route.ts +++ b/src/app/api/citable/route.ts @@ -2,12 +2,7 @@ import { NextResponse } from "next/server"; import { getBenchmarks } from "@/data/benchmarks"; import { SITE } from "@/data/site"; import { AllBenchmarksDraftError } from "@/lib/spec"; -import { - fieldValue, - headlineSentence, - isInsufficient, - leader, -} from "@/lib/citation"; +import { citeBundle, fieldValue, leader, headlineSentence } from "@/lib/citation"; import { clientKey, rateLimit, tooManyRequests } from "@/lib/rate-limit"; export const runtime = "nodejs"; @@ -52,21 +47,29 @@ export async function GET(req: Request) { throw err; } const data = benches.map((b) => { - const insufficient = isInsufficient(b); - const top = insufficient ? null : leader(b); - const status: "live" | "draft" | "insufficient" = insufficient - ? "insufficient" - : b.status; + const top = leader(b); + // Insufficient aggregate: explicitly null the value + leader so + // downstream LLM agents and journalists do not quote a number drawn + // from an undersized field. The headline sentence is rewritten to + // "insufficient data" by headlineSentence above. + const insufficient = b.dataConfidence === "insufficient"; return { slug: b.slug, title: b.title, category: b.category, metric: b.metric, unit: b.unit, - status, + status: b.status, value: insufficient ? null : fieldValue(b), - leader: top ? { name: top.name, slug: top.slug, value: top.value } : null, - sampleSize: insufficient ? 0 : b.sampleSize, + leader: + insufficient + ? null + : top + ? { name: top.name, slug: top.slug, value: top.value } + : null, + sampleSize: b.sampleSize, + expectedN: b.expectedN, + dataConfidence: b.dataConfidence, asOf: b.lastRunAt, headline: headlineSentence(b), url: `${SITE.url}/benchmarks/${b.slug}`, @@ -74,6 +77,7 @@ export async function GET(req: Request) { ogImage: `${SITE.url}/api/og/${b.slug}`, source: b.source, license: "CC-BY-4.0", + cite: citeBundle(b, SITE.url), }; }); diff --git a/src/app/api/stat/[slug]/route.ts b/src/app/api/stat/[slug]/route.ts index 14d6c7d7c..b53c5767a 100644 --- a/src/app/api/stat/[slug]/route.ts +++ b/src/app/api/stat/[slug]/route.ts @@ -3,9 +3,9 @@ import { getBenchmark } from "@/data/benchmarks"; import { SITE } from "@/data/site"; import { citationQuote, + citeBundle, fieldValue, headlineSentence, - isInsufficient, leader, sparklineFor, } from "@/lib/citation"; @@ -20,16 +20,6 @@ export const revalidate = 60; * Single benchmark as a citable atomic unit. Designed to fit into one * agent tool call: ranked providers, sparkline, methodology link, * pre-formatted attribution string, and stable citation URL. - * - * Status field semantics: - * "live" - usable measurement, leader / value populated. - * "draft" - spec author has not published (editorialStatus draft). - * "insufficient" - editorially live but the harness has no usable - * sample yet (every provider p50 = 0, or runtime - * status flipped to draft mid-cycle). value, leader - * and rankings p50 are nulled so a consumer cannot - * accidentally cite a fabricated winner. The shape - * of the response is preserved. */ export async function GET( req: Request, @@ -53,46 +43,8 @@ export async function GET( ); } - const insufficient = isInsufficient(b); - const top = insufficient ? null : leader(b); - const value = insufficient ? null : fieldValue(b); - // Status surfaced to consumers: "insufficient" wins over the raw - // runtime "live" flag when the predicate fires, so /api/stat stops - // claiming live data for a bench whose harness has nothing to show. - const status: "live" | "draft" | "insufficient" = insufficient - ? "insufficient" - : b.status; - - // Rankings: when insufficient we still return one entry per provider - // (shape preserved for any consumer that diff-tracks the provider set) - // but every p50 is nulled to drive home that no comparison is possible. - const rankings = insufficient - ? b.results.map((r) => ({ - name: r.name, - slug: r.slug, - type: r.type ?? null, - layer: r.layer ?? null, - tag: r.tag ?? null, - ms: { p50: null, p90: null, p99: null, mean: null }, - successRate: r.successRate, - sampleSize: r.sampleSize ?? null, - })) - : b.results - .filter((r) => r.ms.p50 > 0) - .sort((a, c) => - b.higherIsBetter ? c.ms.p50 - a.ms.p50 : a.ms.p50 - c.ms.p50, - ) - .map((r) => ({ - name: r.name, - slug: r.slug, - type: r.type ?? null, - layer: r.layer ?? null, - tag: r.tag ?? null, - ms: r.ms, - successRate: r.successRate, - sampleSize: r.sampleSize, - })); - + const top = leader(b); + const insufficient = b.dataConfidence === "insufficient"; const payload = { slug: b.slug, title: b.title, @@ -100,27 +52,44 @@ export async function GET( category: b.category, metric: b.metric, unit: b.unit, - status, + status: b.status, higherIsBetter: b.higherIsBetter, - value, - leader: top, - rankings, - sparkline: insufficient ? [] : sparklineFor(b, top?.slug), - sampleSize: insufficient ? 0 : b.sampleSize, + // Aggregate is "insufficient" (median per-provider sample health + // below 10 percent of expected_n): refuse to publish a value or + // leader; the headline is rewritten by headlineSentence so the + // agent / journalist reads "insufficient data" instead of quoting + // a number drawn from undersized samples. + value: insufficient ? null : fieldValue(b), + leader: insufficient ? null : top, + rankings: b.results + .filter((r) => r.ms.p50 > 0) + // Drop "insufficient" rows from the machine-readable ranking too: + // a row that the page hides from the leaderboard must not surface + // here either. + .filter((r) => r.dataConfidence !== "insufficient") + .sort((a, c) => (b.higherIsBetter ? c.ms.p50 - a.ms.p50 : a.ms.p50 - c.ms.p50)) + .map((r) => ({ + name: r.name, + slug: r.slug, + ms: r.ms, + successRate: r.successRate, + sampleSize: r.sampleSize, + sampleHealth: r.sampleHealth, + dataConfidence: r.dataConfidence, + })), + sparkline: sparklineFor(b, top?.slug), + sampleSize: b.sampleSize, + expectedN: b.expectedN, + dataConfidence: b.dataConfidence, asOf: b.lastRunAt, headline: headlineSentence(b), quote: citationQuote(b, SITE.url), + cite: citeBundle(b, SITE.url), pageUrl: `${SITE.url}/benchmarks/${b.slug}`, ogImage: `${SITE.url}/api/og/${b.slug}`, source: b.source, methodology: b.methodology, license: "CC-BY-4.0", - // Per-chain leader / worst. Empty for benches without a chain - // dimension. Used by the HF publisher to populate chain_leaders. - // Insufficient benches null both since the underlying rankings - // are not citable. - bestPerChain: insufficient ? null : (b.bestPerChain ?? null), - worstPerChain: insufficient ? null : (b.worstPerChain ?? null), }; return NextResponse.json(payload, { diff --git a/src/app/benchmarks/category/[cat]/page.tsx b/src/app/benchmarks/category/[cat]/page.tsx new file mode 100644 index 000000000..f001cc93b --- /dev/null +++ b/src/app/benchmarks/category/[cat]/page.tsx @@ -0,0 +1,112 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { getBenchmarksSafe, toBenchmarkCardData } from "@/data/benchmarks"; +import { BenchmarkGrid } from "@/components/benchmark-grid"; +import { Breadcrumb } from "@/components/breadcrumb"; +import { safeJsonLd, buildItemListJsonLd } from "@/lib/jsonld"; +import { SITE } from "@/data/site"; +import { pageMetadata } from "@/lib/page-metadata"; +import { capDescription } from "@/lib/seo-text"; +import { CATEGORIES, CATEGORY_BY_SLUG } from "@/lib/categories"; + +/** + * Per-category hub page. One static route per entry in `CATEGORIES` so + * each architectural slice (RPCs, Bridges, Blockchains, ...) has a + * crawlable, linkable URL. Lets external sites and internal nav deep + * link directly into "all Solana RPC benchmarks" instead of relying on + * the client-only filter that lives on `/benchmarks`. + * + * Prerendered at build time via `generateStaticParams` (closed list, no + * Prom calls needed). ISR refresh aligned with the main hub so a freshly + * added bench surfaces here on the same cadence. + */ + +export const revalidate = 60; + +type Params = { cat: string }; + +export async function generateStaticParams() { + // Closed enum, generated even when a category currently has zero live + // benches (e.g. Wallets at time of writing). The page-level fallback + // below 404s empty categories so crawlers don't index thin pages. + return CATEGORIES.map((c) => ({ cat: c.slug })); +} + +export async function generateMetadata({ + params, +}: { + params: Promise; +}): Promise { + const { cat } = await params; + const entry = CATEGORY_BY_SLUG.get(cat); + if (!entry) return {}; + const description = capDescription(entry.description, 158); + return pageMetadata({ + path: `/benchmarks/category/${entry.slug}`, + title: `${entry.heading}`, + description, + }); +} + +export default async function BenchmarkCategoryPage({ + params, +}: { + params: Promise; +}) { + const { cat } = await params; + const entry = CATEGORY_BY_SLUG.get(cat); + if (!entry) notFound(); + + const all = await getBenchmarksSafe(); + const benchmarks = all.filter((b) => b.category === entry.label); + // Empty-category guard: a category in the enum that has no live bench + // yet returns 404 so the crawler doesn't land on a thin page. The + // category still ships in `generateStaticParams` so adding the first + // bench to it lights up the URL without a redeploy gate. + if (benchmarks.length === 0) notFound(); + + const url = `${SITE.url}/benchmarks/category/${entry.slug}`; + const jsonLd = buildItemListJsonLd({ + name: `${entry.heading} on OpenChainBench`, + url, + description: entry.description, + items: benchmarks.map((b) => ({ + name: b.title, + url: `${SITE.url}/benchmarks/${b.slug}`, + })), + breadcrumb: [ + { name: "Home", url: `${SITE.url}/` }, + { name: "All benchmarks", url: `${SITE.url}/benchmarks` }, + { name: entry.heading, url }, + ], + }); + + return ( +
+