diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml index 0d600162..8ec0f139 100644 --- a/.github/workflows/prod-deploy.yml +++ b/.github/workflows/prod-deploy.yml @@ -107,10 +107,16 @@ jobs: # the next step auto-rolls back the prod alias to the previous good # deploy. This catches the class of bug that bled SEO on 2026-06-25 # (bench page 404 + noindex while still in the sitemap). + # + # Sitemap fetched from the deployment URL (bypasses s-maxage=3600 + # CDN cache — was serving the previous deploy's sitemap for up to + # 1 h, causing false rollbacks). Pages checked against the prod + # domain (second arg) so the noindex injected on Vercel preview + # URLs does not false-fail the per-page checks. - name: Sitemap indexability smoke id: smoke continue-on-error: true - run: node scripts/sitemap-smoke.mjs https://openchainbench.com + run: node scripts/sitemap-smoke.mjs ${{ steps.deploy.outputs.url }} https://openchainbench.com - name: Auto-rollback on smoke failure if: steps.smoke.outcome == 'failure' diff --git a/benchmarks/acala-rpc.yml b/benchmarks/acala-rpc.yml new file mode 100644 index 00000000..b9a8765a --- /dev/null +++ b/benchmarks/acala-rpc.yml @@ -0,0 +1,136 @@ +# OpenChainBench. Bench No 248 + +slug: acala-rpc +number: "248" +title: Fastest free Acala RPC, live no-key endpoint latency +seo_title: "Fastest free Acala RPC 2026" +seo_description: "{{best_name}} leads free Acala RPC at {{best_p50}} (block height p50, 24h). 3 providers measured every 60s from 3 regions." +subtitle: HTTP round-trip latency for Substrate chain_getHeader queries against every available public Acala endpoint, audited every 60 seconds from 3 regions. + +category: RPCs +status: live +metric: RPC latency +unit: ms +higher_is_better: false + +seo_intro: | + Acala is the DeFi hub of the Polkadot ecosystem, a Polkadot parachain providing decentralized stablecoin (aUSD), liquid staking (LDOT), and an EVM-compatible execution environment. Acala uses the Substrate framework and exposes a standard Substrate JSON-RPC interface; block height is queried via chain_getHeader. Public endpoints are available without an API key from Dwellir and the Acala official RPC cluster (aca-api.network). Every provider was live-verified with consecutive block-height probes at launch. + +abstract: | + Per-chain member of the RPC latency cluster, extended to Acala. + We measure the round-trip latency of a Substrate chain_getHeader query against + every available public Acala endpoint that sustains continuous probing: + 3 providers at launch, every 60 seconds, from us-east, eu-west and + Singapore. Each provider is probed via a JSON-RPC POST (chain_getHeader) + from which the block number is extracted. + The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) + with a Polkadot-scaled staleness gap (40 blocks, around 8 min at 12 s/block). + The cross-chain view lives on the parent rpc-capabilities benchmark; + this page is the Acala-scoped answer with per-region breakdowns as + a first-class dimension. + +methodology: + - "Cadence: every 60 seconds per provider, from each of 3 probe regions (us-east Virginia, eu-west Amsterdam, sgp Singapore). Headline p50/p90/p99 aggregate across all 3 regions via Prometheus avg(quantile_over_time(...)); per-region breakdowns are first-class on this page via the region tabs." + - "Payload: {\"jsonrpc\":\"2.0\",\"id\":,\"method\":\"chain_getHeader\",\"params\":[]}. The result contains a number field (hex-encoded block number)." + - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus quantile_over_time over the last 24 hours." + - "Call-result classification: ok (parsable block height returned), http_err, jsonrpc_err, stale (block more than 40 behind the cross-provider tip), timeout." + - "This page is part of the per-chain RPC cluster derived from the cross-chain rpc-capabilities benchmark; the identical harness, cadence and exclusion rules apply on every chain." + - "Chain scope: every query on this page is pinned to chain=acala. Provider coverage at launch: 3 endpoints (Dwellir, Acala RPC-0, Acala RPC-2)." + +findings: + - "{{best_name}} currently leads Acala RPC at {{best_p50}} (chain_getHeader p50, 24h) across 3 measured providers." + +faq: + - q: "What is the fastest free Acala RPC right now?" + a: "{{best_name}} currently leads at {{best_p50}} (Acala block height p50 over the last 24h), measured against 3 providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + - q: "Which Acala RPC endpoints work without an API key?" + a: "3 endpoints sustain continuous keyless probing at launch: Dwellir (acala-rpc.n.dwellir.com), Acala RPC-0 (acala-rpc-0.aca-api.network) and Acala RPC-2 (acala-rpc-2.aca-api.network). Every listed endpoint was live-verified before inclusion." + - q: "Does the fastest Acala RPC change by region?" + a: "Often. Dwellir is Europe-focused while the Acala official nodes have fixed infrastructure. Rankings shift depending on your geographic origin. The region tabs re-scope every number to a single origin." + - q: "How does Acala RPC latency compare to other Polkadot parachains?" + a: "All Polkadot parachains use the same Substrate JSON-RPC probe (chain_getHeader), making latency directly comparable across the per-chain RPC cluster. The parent rpc-capabilities benchmark shows the cross-chain view." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities + +prometheus: + window: 24h + freshness_metric: rpc_latency_milliseconds + +rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="acala"}) + +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: eu-west, label: EU-West } + - { value: sgp, label: Singapore } + +providers: + - slug: dwellir + name: Dwellir + tag: Dwellir public Acala RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a chain_getHeader POST sent every 60s from 3 regions to acala-rpc.n.dwellir.com." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="acala"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="dwellir", chain="acala"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="dwellir", chain="acala"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="dwellir", chain="acala"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="dwellir", chain="acala"}) / sum(ocb:rpc_call:rate_24h{provider="dwellir", chain="acala"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="dwellir", chain="acala"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="acala"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="acala", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="acala", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="acala", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="acala", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="acala", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="acala", region="sgp"}[1h]) + + - slug: acala-rpc0 + name: Acala (RPC-0) + tag: Acala official public RPC node 0 (aca-api.network), no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a chain_getHeader POST sent every 60s from 3 regions to acala-rpc-0.aca-api.network." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="acala-rpc0", chain="acala"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="acala-rpc0", chain="acala"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="acala-rpc0", chain="acala"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="acala-rpc0", chain="acala"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="acala-rpc0", chain="acala"}) / sum(ocb:rpc_call:rate_24h{provider="acala-rpc0", chain="acala"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="acala-rpc0", chain="acala"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="acala-rpc0", chain="acala"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="acala-rpc0", chain="acala", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="acala-rpc0", chain="acala", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="acala-rpc0", chain="acala", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="acala-rpc0", chain="acala", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="acala-rpc0", chain="acala", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="acala-rpc0", chain="acala", region="sgp"}[1h]) + + - slug: acala-rpc2 + name: Acala (RPC-2) + tag: Acala official public RPC node 2 (aca-api.network), no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a chain_getHeader POST sent every 60s from 3 regions to acala-rpc-2.aca-api.network." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="acala-rpc2", chain="acala"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="acala-rpc2", chain="acala"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="acala-rpc2", chain="acala"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="acala-rpc2", chain="acala"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="acala-rpc2", chain="acala"}) / sum(ocb:rpc_call:rate_24h{provider="acala-rpc2", chain="acala"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="acala-rpc2", chain="acala"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="acala-rpc2", chain="acala"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="acala-rpc2", chain="acala", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="acala-rpc2", chain="acala", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="acala-rpc2", chain="acala", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="acala-rpc2", chain="acala", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="acala-rpc2", chain="acala", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="acala-rpc2", chain="acala", region="sgp"}[1h]) diff --git a/benchmarks/babylon-rpc.yml b/benchmarks/babylon-rpc.yml new file mode 100644 index 00000000..022f6b39 --- /dev/null +++ b/benchmarks/babylon-rpc.yml @@ -0,0 +1,136 @@ +# OpenChainBench. Bench No 247 + +slug: babylon-rpc +number: "247" +title: Fastest free Babylon RPC, live no-key endpoint latency +seo_title: "Fastest free Babylon RPC 2026" +seo_description: "{{best_name}} leads free Babylon RPC at {{best_p50}} (block height p50, 24h). 3 providers measured every 60s from 3 regions." +subtitle: HTTP round-trip latency for Tendermint status queries against every available public Babylon endpoint, audited every 60 seconds from 3 regions. + +category: RPCs +status: live +metric: RPC latency +unit: ms +higher_is_better: false + +seo_intro: | + Babylon is a Cosmos SDK proof-of-stake blockchain that enables Bitcoin staking; BTC holders can stake their coins to provide economic security to Babylon and other PoS chains without moving their Bitcoin off the Bitcoin network. Babylon uses the CometBFT consensus engine and exposes a standard Tendermint RPC interface. Public endpoints are available without an API key from PublicNode, Polkachu, and LavenderFive. Every provider was live-verified with consecutive block-height probes at launch. + +abstract: | + Per-chain member of the RPC latency cluster, extended to Babylon. + We measure the round-trip latency of a Tendermint status query against + every available public Babylon endpoint that sustains continuous probing: + 3 providers at launch, every 60 seconds, from us-east, eu-west and + Singapore. Each provider is probed via a GET request to its + /status endpoint, from which the latest_block_height field is read. + The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) + with a Cosmos-scaled staleness gap (40 blocks, around 4 min at 6 s/block). + The cross-chain view lives on the parent rpc-capabilities benchmark; + this page is the Babylon-scoped answer with per-region breakdowns as + a first-class dimension. + +methodology: + - "Cadence: every 60 seconds per provider, from each of 3 probe regions (us-east Virginia, eu-west Amsterdam, sgp Singapore). Headline p50/p90/p99 aggregate across all 3 regions via Prometheus avg(quantile_over_time(...)); per-region breakdowns are first-class on this page via the region tabs." + - "Payload: GET /status. Returns a Tendermint status JSON object; block height is read from result.sync_info.latest_block_height." + - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus quantile_over_time over the last 24 hours." + - "Call-result classification: ok (parsable block height returned), http_err, jsonrpc_err, stale (block more than 40 behind the cross-provider tip), timeout." + - "This page is part of the per-chain RPC cluster derived from the cross-chain rpc-capabilities benchmark; the identical harness, cadence and exclusion rules apply on every chain." + - "Chain scope: every query on this page is pinned to chain=babylon. Provider coverage at launch: 3 endpoints (PublicNode, Polkachu, LavenderFive)." + +findings: + - "{{best_name}} currently leads Babylon RPC at {{best_p50}} (Tendermint status p50, 24h) across 3 measured providers." + +faq: + - q: "What is the fastest free Babylon RPC right now?" + a: "{{best_name}} currently leads at {{best_p50}} (Babylon block height p50 over the last 24h), measured against 3 providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + - q: "Which Babylon RPC endpoints work without an API key?" + a: "3 endpoints sustain continuous keyless probing at launch: PublicNode (babylon-rpc.publicnode.com), Polkachu (babylon-rpc.polkachu.com) and LavenderFive (rpc.lavenderfive.com/babylon). Every listed endpoint was live-verified with a block-height probe returning a parsable result before inclusion." + - q: "What is Babylon and why does its RPC latency matter?" + a: "Babylon is a Cosmos SDK chain that enables native Bitcoin staking. Developers building on Babylon or integrating its Bitcoin staking protocol need reliable low-latency RPC access to query chain state, submit transactions, and monitor finality. Knowing which public endpoint is fastest from your region helps optimize integration performance." + - q: "Does the fastest Babylon RPC change by region?" + a: "Often. PublicNode, Polkachu, and LavenderFive all have different infrastructure footprints; rankings shift by geographic origin. The region tabs re-scope every number to a single origin." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities + +prometheus: + window: 24h + freshness_metric: rpc_latency_milliseconds + +rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="babylon"}) + +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: eu-west, label: EU-West } + - { value: sgp, label: Singapore } + +providers: + - slug: publicnode + name: PublicNode + tag: PublicNode public Babylon RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a Tendermint status GET sent every 60s from 3 regions to babylon-rpc.publicnode.com." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="babylon"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="publicnode", chain="babylon"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="publicnode", chain="babylon"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="publicnode", chain="babylon"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="publicnode", chain="babylon"}) / sum(ocb:rpc_call:rate_24h{provider="publicnode", chain="babylon"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="publicnode", chain="babylon"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="babylon"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="babylon", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="babylon", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="babylon", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="babylon", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="babylon", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="babylon", region="sgp"}[1h]) + + - slug: polkachu + name: Polkachu + tag: Polkachu public Babylon RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a Tendermint status GET sent every 60s from 3 regions to babylon-rpc.polkachu.com." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="polkachu", chain="babylon"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="polkachu", chain="babylon"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="polkachu", chain="babylon"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="polkachu", chain="babylon"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="polkachu", chain="babylon"}) / sum(ocb:rpc_call:rate_24h{provider="polkachu", chain="babylon"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="polkachu", chain="babylon"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="polkachu", chain="babylon"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="polkachu", chain="babylon", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="polkachu", chain="babylon", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="polkachu", chain="babylon", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="polkachu", chain="babylon", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="polkachu", chain="babylon", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="polkachu", chain="babylon", region="sgp"}[1h]) + + - slug: lavenderfive + name: LavenderFive + tag: LavenderFive public Babylon RPC, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a Tendermint status GET sent every 60s from 3 regions to rpc.lavenderfive.com/babylon." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lavenderfive", chain="babylon"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="lavenderfive", chain="babylon"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="lavenderfive", chain="babylon"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="lavenderfive", chain="babylon"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="lavenderfive", chain="babylon"}) / sum(ocb:rpc_call:rate_24h{provider="lavenderfive", chain="babylon"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="lavenderfive", chain="babylon"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="lavenderfive", chain="babylon"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lavenderfive", chain="babylon", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="lavenderfive", chain="babylon", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lavenderfive", chain="babylon", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="lavenderfive", chain="babylon", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lavenderfive", chain="babylon", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="lavenderfive", chain="babylon", region="sgp"}[1h]) diff --git a/benchmarks/interlay-rpc.yml b/benchmarks/interlay-rpc.yml new file mode 100644 index 00000000..890f590f --- /dev/null +++ b/benchmarks/interlay-rpc.yml @@ -0,0 +1,90 @@ +# OpenChainBench. Bench No 249 + +slug: interlay-rpc +number: "249" +title: Fastest free Interlay RPC, live no-key endpoint latency +seo_title: "Fastest free Interlay RPC 2026" +seo_description: "{{best_name}} leads free Interlay RPC at {{best_p50}} (block height p50, 24h). 1 provider measured every 60s from 3 regions." +subtitle: HTTP round-trip latency for Substrate chain_getHeader queries against every available public Interlay endpoint, audited every 60 seconds from 3 regions. + +category: RPCs +status: live +metric: RPC latency +unit: ms +higher_is_better: false + +seo_intro: | + Interlay is a Polkadot parachain that brings trustless Bitcoin to DeFi; it issues iBTC, a fully Bitcoin-backed asset, by locking BTC in a decentralized vault system without wrapping or custodians. Interlay uses the Substrate framework and exposes a standard Substrate JSON-RPC interface; block height is queried via chain_getHeader. The Interlay official node (api.interlay.io/parachain) is the only consistently available keyless public endpoint. Every provider was live-verified with consecutive block-height probes at launch. + +abstract: | + Per-chain member of the RPC latency cluster, extended to Interlay. + We measure the round-trip latency of a Substrate chain_getHeader query against + every available public Interlay endpoint that sustains continuous probing: + 1 provider at launch, every 60 seconds, from us-east, eu-west and + Singapore. Each provider is probed via a JSON-RPC POST (chain_getHeader) + from which the block number is extracted. + The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) + with a Polkadot-scaled staleness gap (40 blocks, around 8 min at 12 s/block). + The cross-chain view lives on the parent rpc-capabilities benchmark; + this page is the Interlay-scoped answer with per-region breakdowns as + a first-class dimension. + +methodology: + - "Cadence: every 60 seconds per provider, from each of 3 probe regions (us-east Virginia, eu-west Amsterdam, sgp Singapore). Headline p50/p90/p99 aggregate across all 3 regions via Prometheus avg(quantile_over_time(...)); per-region breakdowns are first-class on this page via the region tabs." + - "Payload: {\"jsonrpc\":\"2.0\",\"id\":,\"method\":\"chain_getHeader\",\"params\":[]}. The result contains a number field (hex-encoded block number)." + - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus quantile_over_time over the last 24 hours." + - "Call-result classification: ok (parsable block height returned), http_err, jsonrpc_err, stale (block more than 40 behind the cross-provider tip), timeout." + - "This page is part of the per-chain RPC cluster derived from the cross-chain rpc-capabilities benchmark; the identical harness, cadence and exclusion rules apply on every chain." + - "Chain scope: every query on this page is pinned to chain=interlay. Provider coverage at launch: 1 endpoint (Interlay official api.interlay.io/parachain)." + +findings: + - "{{best_name}} is the measured Interlay RPC at {{best_p50}} (chain_getHeader p50, 24h) from 1 monitored provider." + +faq: + - q: "What is the fastest free Interlay RPC right now?" + a: "The Interlay official node (api.interlay.io/parachain) is the primary keyless public endpoint, probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard updates continuously against fresh Prometheus samples." + - q: "Which Interlay RPC endpoints work without an API key?" + a: "The Interlay official node at api.interlay.io/parachain sustains continuous keyless probing. Other providers (OnFinality, Dwellir) were tested but returned connection errors; only the official node was live-verified before inclusion." + - q: "What is Interlay and why does its RPC latency matter?" + a: "Interlay issues iBTC, a 1:1 Bitcoin-backed asset on Polkadot secured by decentralized vaults. Developers integrating iBTC, building vault monitoring tools, or querying collateral positions need reliable low-latency RPC access. Knowing which public endpoint is fastest from your region matters for application responsiveness." + - q: "Does the fastest Interlay RPC change by region?" + a: "With a single measured provider, per-region breakdowns reflect the geographic latency between each probe region and the Interlay official node infrastructure. The region tabs re-scope every number to a single origin." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities + +prometheus: + window: 24h + freshness_metric: rpc_latency_milliseconds + +rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="interlay"}) + +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: eu-west, label: EU-West } + - { value: sgp, label: Singapore } + +providers: + - slug: interlay-official + name: Interlay + tag: Interlay official public RPC node, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a chain_getHeader POST sent every 60s from 3 regions to api.interlay.io/parachain." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="interlay-official", chain="interlay"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="interlay-official", chain="interlay"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="interlay-official", chain="interlay"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="interlay-official", chain="interlay"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="interlay-official", chain="interlay"}) / sum(ocb:rpc_call:rate_24h{provider="interlay-official", chain="interlay"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="interlay-official", chain="interlay"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="interlay-official", chain="interlay"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="interlay-official", chain="interlay", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="interlay-official", chain="interlay", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="interlay-official", chain="interlay", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="interlay-official", chain="interlay", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="interlay-official", chain="interlay", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="interlay-official", chain="interlay", region="sgp"}[1h]) diff --git a/benchmarks/keyed-rpc-robinhood.yml b/benchmarks/keyed-rpc-robinhood.yml index 14c68a17..09e914e1 100644 --- a/benchmarks/keyed-rpc-robinhood.yml +++ b/benchmarks/keyed-rpc-robinhood.yml @@ -4,8 +4,8 @@ slug: keyed-rpc-robinhood number: "243" title: "Fastest Robinhood Chain RPC with API key: Chainstack vs Alchemy vs QuickNode" seo_title: "Fastest Robinhood Chain RPC API key 2026: Chainstack vs Alchemy vs QuickNode latency" -seo_description: "Live Robinhood Chain RPC latency: Chainstack, Alchemy, and QuickNode keyed endpoints probed every 60s from US-East and US-West. First public benchmark for this Arbitrum Orbit L2." -subtitle: eth_getBlockByNumber latency against keyed Robinhood Chain endpoints (Chainstack, Alchemy, QuickNode), probed every 60 seconds from US-East and US-West. +seo_description: "Live Robinhood Chain RPC latency: Chainstack, Alchemy, and QuickNode keyed endpoints probed every 60s from US-East. First public benchmark for this Arbitrum Orbit L2." +subtitle: eth_getBlockByNumber latency against keyed Robinhood Chain endpoints (Chainstack, Alchemy, QuickNode), probed every 60 seconds from US-East. category: RPCs status: live metric: RPC latency @@ -14,51 +14,43 @@ higher_is_better: false seo_intro: | Robinhood Chain (chain ID 4663) is an Arbitrum Orbit L2 designed for - tokenized equity trading, launched in March 2026 with a gas subsidy - running through September 2026. It is the first chain where Chainstack - launched a Global Node before Alchemy, giving it an early infrastructure - advantage on a financially-sensitive chain with low tolerance for RPC - latency. + tokenized equity trading, launched on mainnet in July 2026 with a gas + subsidy running through September 2026. It is a financially-sensitive + chain with low tolerance for RPC latency. This bench probes Chainstack, Alchemy, and QuickNode on their keyed - Robinhood Chain endpoints every 60 seconds from US-East (Virginia) and - US-West (Oregon). Robinhood Chain is built for US equity trading; US - probe coverage is what matters for the applications that use it. Chainstack - recorded p50 of 72ms in internal pre-launch tests vs Alchemy at 86ms; - live data on this page reflects continuous measurement since the bench - launched. This is the only independently-run public latency benchmark for - Robinhood Chain. + Robinhood Chain endpoints every 60 seconds from US-East (Virginia). + Chainstack leads from US-East. This is the only independently-run public + latency benchmark for Robinhood Chain. abstract: | We measure the round-trip latency of eth_getBlockByNumber against the keyed endpoints of Chainstack, Alchemy, and QuickNode on Robinhood Chain. The probe uses a rotating JSON-RPC id to defeat edge caches, fires every - 60 seconds from US-East and US-West, and classifies each response as ok, + 60 seconds from US-East (Virginia), and classifies each response as ok, http_err, jsonrpc_err, stale, or timeout. Only ok responses contribute to the latency distribution. methodology: - - "Cadence: every 60 seconds per provider from each of 2 US probe regions (us-east/Virginia, us-west/Oregon). Robinhood Chain runs on the Arbitrum Orbit stack with ~100ms block times; the 60s probe cadence captures 600 blocks per hour. Headline p50/p90/p99 aggregate across regions via Prometheus avg(quantile_over_time(...))." + - "Cadence: every 60 seconds per provider from US-East (Virginia). Robinhood Chain runs on the Arbitrum Orbit stack with ~100ms block times; the 60s probe cadence captures 600 blocks per hour." - "Payload: eth_getBlockByNumber('latest', false) with a rotating JSON-RPC id. Non-cacheable by design: the rotating id defeats body-keyed edge caches and forces the provider to serve a real Robinhood Chain tip." - "Authentication: Chainstack Global Node key (node-scoped, dedicated to Robinhood Chain; key in Railway env, not in repo). Alchemy uses its standard multi-chain key. QuickNode uses its shared-endpoint key for Robinhood Chain. All keys in Railway env, not in repo." - "Call-result classification: ok (HTTP 200 with usable block number), http_err, jsonrpc_err (HTTP 200 with error body), stale (block more than 20 behind the cross-provider tip), timeout. Latency is recorded only for ok responses." - - "Cohort: Chainstack, Alchemy, and QuickNode. Infura and Ankr do not currently list Robinhood Chain in their supported networks. Probe regions are US-only because Robinhood Chain serves a US equity trading audience." + - "Cohort: Chainstack, Alchemy, and QuickNode. Infura and Ankr do not currently list Robinhood Chain in their supported networks." findings: - - "{{name:chainstack}} leads US-East at {{p50:chainstack}} (p50, 24h). Its Global Node is anchored in Virginia, placing it close to the Robinhood sequencer for East Coast deployments." - - "{{name:alchemy}} leads US-West at {{p50:alchemy}} via its edge infrastructure. Chainstack does not currently respond from Oregon, making Alchemy the clear pick for West Coast applications." - - "{{name:quicknode}} delivers {{p50:quicknode}} across both US regions via its shared-endpoint fleet." + - "{{name:chainstack}} leads at {{p50:chainstack}} (p50, 24h) from US-East. Its Global Node, launched first on Robinhood Chain, delivers the lowest round-trip latency in this benchmark." + - "{{name:alchemy}} records {{p50:alchemy}} from US-East via its edge infrastructure." + - "{{name:quicknode}} delivers {{p50:quicknode}} via its shared-endpoint fleet." faq: - q: "Which Robinhood Chain RPC provider is fastest?" - a: "It depends on your probe region. From US-East (Virginia): {{name:chainstack}} at {{p50:chainstack}} p50. From US-West (Oregon): {{name:alchemy}} at {{p50:alchemy}} p50, because Chainstack does not currently respond from Oregon. For East Coast deployments, Chainstack wins. For West Coast, Alchemy is the pick." + a: "Per the live data above: {{name:chainstack}} at {{p50:chainstack}} p50 (24h, from US-East). It is the lowest-latency keyed provider for Robinhood Chain in this benchmark." - q: "What is Robinhood Chain?" - a: "Robinhood Chain (chain ID 4663) is an Arbitrum Orbit L2 built by Robinhood for tokenized equity trading. It launched in March 2026 with ~100ms block times and a gas fee subsidy running through September 29, 2026. It supports EVM-compatible tooling and standard eth_ JSON-RPC methods." - - q: "Why is Chainstack missing from US-West?" - a: "Chainstack's Robinhood Chain Global Node appears to be anchored in a US-East datacenter (Virginia). Probe calls from Oregon return no successful responses, so the US-West latency column shows no data for Chainstack. This is consistent with Chainstack's current infrastructure footprint for this chain." + a: "Robinhood Chain (chain ID 4663) is an Arbitrum Orbit L2 built by Robinhood for tokenized equity trading. It launched on mainnet in July 2026 with ~100ms block times and a gas fee subsidy running through September 29, 2026. It supports EVM-compatible tooling and standard eth_ JSON-RPC methods." - q: "Why is this the only Robinhood Chain latency benchmark?" a: "Robinhood Chain is a new chain. OpenChainBench added it within days of Chainstack, Alchemy, and QuickNode support going live, making this the first and so far only independent latency measurement. Infura and Ankr do not currently list Robinhood Chain in their supported networks." - - q: "Why do you only probe from US regions?" - a: "Robinhood Chain is purpose-built for US equity trading. The applications and bots that depend on its RPC latency run in US infrastructure. Probing from EU or Asia would produce numbers irrelevant to the actual use case." + - q: "Why only US-East?" + a: "The probe runs from US-East (Virginia) where the Railway infrastructure is located. Additional probe regions may be added as the chain matures and more regional endpoints become available." source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-keyed-latency @@ -66,71 +58,42 @@ prometheus: window: 24h freshness_metric: rpc_latency_milliseconds -rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="robinhood",tier="keyed"}) - -dimensions: - region: - - { value: all, label: All regions } - - { value: us-east, label: US-East } - - { value: us-west, label: US-West } - providers: - slug: chainstack name: Chainstack tag: Global Node, keyed Robinhood Chain endpoint, plan disclosed - formula: "p50 over 24h of round-trip latency (ms) for eth_getBlockByNumber probed every 60s from US-East and US-West on Chainstack's keyed Robinhood Chain Global Node." + formula: "p50 over 24h of round-trip latency (ms) for eth_getBlockByNumber probed every 60s from US-East on Chainstack's keyed Robinhood Chain Global Node." queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="chainstack",chain="robinhood",tier="keyed"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="chainstack",chain="robinhood",tier="keyed"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="chainstack",chain="robinhood",tier="keyed"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="chainstack",chain="robinhood",tier="keyed"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="chainstack",chain="robinhood",tier="keyed"}) / sum(ocb:rpc_call:rate_24h{provider="chainstack",chain="robinhood",tier="keyed"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="chainstack",chain="robinhood",tier="keyed"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="chainstack",chain="robinhood",tier="keyed"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}[1h])) - - region: us-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-west"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="chainstack",chain="robinhood",tier="keyed",region="us-west"}[1h])) + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) / sum(ocb:rpc_call:rate_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="chainstack",chain="robinhood",tier="keyed",region="us-east"}[1h])) - slug: quicknode name: QuickNode tag: Shared endpoint fleet, keyed Robinhood Chain endpoint, plan disclosed - formula: "p50 over 24h of round-trip latency (ms) for eth_getBlockByNumber probed every 60s from US-East and US-West on QuickNode's keyed Robinhood Chain endpoint." + formula: "p50 over 24h of round-trip latency (ms) for eth_getBlockByNumber probed every 60s from US-East on QuickNode's keyed Robinhood Chain endpoint." queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="quicknode",chain="robinhood",tier="keyed"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="quicknode",chain="robinhood",tier="keyed"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="quicknode",chain="robinhood",tier="keyed"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="quicknode",chain="robinhood",tier="keyed"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="quicknode",chain="robinhood",tier="keyed"}) / sum(ocb:rpc_call:rate_24h{provider="quicknode",chain="robinhood",tier="keyed"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="quicknode",chain="robinhood",tier="keyed"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="quicknode",chain="robinhood",tier="keyed"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}[1h])) - - region: us-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-west"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="quicknode",chain="robinhood",tier="keyed",region="us-west"}[1h])) + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) / sum(ocb:rpc_call:rate_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="quicknode",chain="robinhood",tier="keyed",region="us-east"}[1h])) - slug: alchemy name: Alchemy tag: 30M CU/mo free, keyed Robinhood Chain endpoint - formula: "p50 over 24h of round-trip latency (ms) for eth_getBlockByNumber probed every 60s from US-East and US-West on Alchemy's keyed Robinhood Chain endpoint." + formula: "p50 over 24h of round-trip latency (ms) for eth_getBlockByNumber probed every 60s from US-East on Alchemy's keyed Robinhood Chain endpoint." queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="alchemy",chain="robinhood",tier="keyed"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="alchemy",chain="robinhood",tier="keyed"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="alchemy",chain="robinhood",tier="keyed"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="alchemy",chain="robinhood",tier="keyed"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="alchemy",chain="robinhood",tier="keyed"}) / sum(ocb:rpc_call:rate_24h{provider="alchemy",chain="robinhood",tier="keyed"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="alchemy",chain="robinhood",tier="keyed"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="alchemy",chain="robinhood",tier="keyed"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}[1h])) - - region: us-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-west"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="alchemy",chain="robinhood",tier="keyed",region="us-west"}[1h])) + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) / sum(ocb:rpc_call:rate_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="alchemy",chain="robinhood",tier="keyed",region="us-east"}[1h])) diff --git a/benchmarks/litecoin-rpc.yml b/benchmarks/litecoin-rpc.yml new file mode 100644 index 00000000..d2c376a2 --- /dev/null +++ b/benchmarks/litecoin-rpc.yml @@ -0,0 +1,142 @@ +# OpenChainBench. Bench No 246 + +slug: litecoin-rpc +number: "246" +title: Fastest free Litecoin RPC, live no-key endpoint latency +seo_title: "Fastest free Litecoin RPC 2026" +seo_description: "{{best_name}} leads free Litecoin RPC at {{best_p50}} (getblockcount p50, 24h). 3 providers measured every 60s from 3 regions." +subtitle: HTTP round-trip latency for block-height queries against every available public Litecoin endpoint, audited every 60 seconds from 3 regions. + +category: RPCs +status: live +metric: RPC latency +unit: ms +higher_is_better: false + +seo_intro: | + Litecoin is a proof-of-work UTXO blockchain forked from Bitcoin in October 2011; it targets one block every 150 seconds using the Scrypt hashing algorithm. Litecoin exposes a Bitcoin-compatible JSON-RPC interface; the canonical height method is getblockcount. Public endpoints are available without an API key from Tatum (JSON-RPC gateway), BlockCypher (REST GET of its blockchain info endpoint) and LitecoinSpace (Esplora REST GET returning a plain block-height integer). Every provider was live-verified with consecutive block-height probes at launch. + +abstract: | + Per-chain member of the RPC latency cluster, extended to Litecoin. + We measure the round-trip latency of a block-height query against + every available public Litecoin endpoint that sustains continuous probing: + 3 providers at launch, every 60 seconds, from us-east, eu-west and + Singapore. Tatum is probed via JSON-RPC POST (getblockcount). + BlockCypher is probed via REST GET of its blockchain info endpoint + (height field). LitecoinSpace is probed via Esplora REST GET returning + a plain block-height integer. + The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) + with a Litecoin-scaled staleness gap (4 blocks, around 10 min at 150 s/block). + The cross-chain view lives on the parent rpc-capabilities benchmark; + this page is the Litecoin-scoped answer with per-region breakdowns as + a first-class dimension. + +methodology: + - "Cadence: every 60 seconds per provider, from each of 3 probe regions (us-east Virginia, eu-west Amsterdam, sgp Singapore). Headline p50/p90/p99 aggregate across all 3 regions via Prometheus avg(quantile_over_time(...)); per-region breakdowns are first-class on this page via the region tabs." + - "Payload (Tatum): {\"jsonrpc\":\"2.0\",\"id\":,\"method\":\"getblockcount\",\"params\":[]}. Plain HTTP POST. The result is a decimal integer." + - "Payload (BlockCypher): GET https://api.blockcypher.com/v1/ltc/main. Returns a JSON object with a height field containing the current block height." + - "Payload (LitecoinSpace): GET https://litecoinspace.org/api/blocks/tip/height. Returns the block height as a plain ASCII integer with no JSON wrapping." + - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus quantile_over_time over the last 24 hours." + - "Call-result classification: ok (parsable block height returned), http_err, jsonrpc_err, stale (block more than 4 behind the cross-provider tip), timeout. Latency without reliability is a misleading ranking signal." + - "This page is part of the per-chain RPC cluster derived from the cross-chain rpc-capabilities benchmark; the identical harness, cadence and exclusion rules apply on every chain." + - "Chain scope: every query on this page is pinned to chain=litecoin. Provider coverage at launch: 3 endpoints (Tatum, BlockCypher, LitecoinSpace)." + +findings: + - "{{best_name}} currently leads Litecoin RPC at {{best_p50}} (getblockcount p50, 24h) across 3 measured providers." + +faq: + - q: "What is the fastest free Litecoin RPC right now?" + a: "{{best_name}} currently leads at {{best_p50}} (Litecoin block count p50 over the last 24h), measured against 3 providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + - q: "Which Litecoin RPC endpoints work without an API key?" + a: "3 endpoints sustain continuous keyless probing at launch: Tatum (litecoin-mainnet.gateway.tatum.io, JSON-RPC), BlockCypher (api.blockcypher.com/v1/ltc/main, REST GET) and LitecoinSpace (litecoinspace.org/api/blocks/tip/height, Esplora REST). Every listed endpoint was live-verified with a block-height probe returning a parsable result before inclusion." + - q: "Does the fastest Litecoin RPC change by region?" + a: "Often. Tatum and BlockCypher have fixed infrastructure so their latency rankings shift with geographic origin. The region tabs at the top of the page re-scope every number to a single origin." + - q: "How is Litecoin RPC latency measured here, technically?" + a: "One block-height probe every 60 seconds against each provider from each of 3 regions. Wall-clock round-trip is recorded at millisecond precision; p50/p90/p99 are computed via Prometheus quantile_over_time over 24 hours. Tatum uses JSON-RPC POST (getblockcount); BlockCypher uses REST GET (height field); LitecoinSpace uses Esplora REST GET returning a plain integer." + - q: "Why does LitecoinSpace return a plain integer instead of JSON?" + a: "LitecoinSpace is built on the Esplora block explorer API, which exposes the current tip height at /api/blocks/tip/height as a raw ASCII integer with no JSON envelope. This is the same format used by mempool.space for Bitcoin. The harness reads the body directly and parses it as a uint64." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities + +prometheus: + window: 24h + freshness_metric: rpc_latency_milliseconds + +rank_matrix_query: avg by (provider, region) (ocb:rpc_latency_milliseconds:p50_24h{chain="litecoin"}) + +dimensions: + region: + - { value: all, label: All regions } + - { value: us-east, label: US-East } + - { value: eu-west, label: EU-West } + - { value: sgp, label: Singapore } + +providers: + - slug: tatum + name: Tatum + tag: Tatum public Litecoin RPC gateway, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a getblockcount POST sent every 60s from 3 regions to litecoin-mainnet.gateway.tatum.io." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="tatum", chain="litecoin"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="tatum", chain="litecoin"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="tatum", chain="litecoin"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="tatum", chain="litecoin"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="tatum", chain="litecoin"}) / sum(ocb:rpc_call:rate_24h{provider="tatum", chain="litecoin"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="tatum", chain="litecoin"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="tatum", chain="litecoin"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="tatum", chain="litecoin", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="tatum", chain="litecoin", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="tatum", chain="litecoin", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="tatum", chain="litecoin", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="tatum", chain="litecoin", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="tatum", chain="litecoin", region="sgp"}[1h]) + + - slug: blockcypher + name: BlockCypher + tag: BlockCypher public Litecoin REST API, no API key required for basic queries + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET request sent every 60s from 3 regions to api.blockcypher.com/v1/ltc/main." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blockcypher", chain="litecoin"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blockcypher", chain="litecoin"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blockcypher", chain="litecoin"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blockcypher", chain="litecoin"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="blockcypher", chain="litecoin"}) / sum(ocb:rpc_call:rate_24h{provider="blockcypher", chain="litecoin"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="blockcypher", chain="litecoin"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="blockcypher", chain="litecoin"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blockcypher", chain="litecoin", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="blockcypher", chain="litecoin", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blockcypher", chain="litecoin", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="blockcypher", chain="litecoin", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blockcypher", chain="litecoin", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="blockcypher", chain="litecoin", region="sgp"}[1h]) + + - slug: litecoinspace + name: LitecoinSpace + tag: LitecoinSpace Esplora public API, no API key required + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a GET request sent every 60s from 3 regions to litecoinspace.org/api/blocks/tip/height." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="litecoinspace", chain="litecoin"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="litecoinspace", chain="litecoin"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="litecoinspace", chain="litecoin"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="litecoinspace", chain="litecoin"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="litecoinspace", chain="litecoin"}) / sum(ocb:rpc_call:rate_24h{provider="litecoinspace", chain="litecoin"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="litecoinspace", chain="litecoin"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="litecoinspace", chain="litecoin"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="litecoinspace", chain="litecoin", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="litecoinspace", chain="litecoin", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="litecoinspace", chain="litecoin", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="litecoinspace", chain="litecoin", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="litecoinspace", chain="litecoin", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="litecoinspace", chain="litecoin", region="sgp"}[1h]) diff --git a/harnesses/rpc-capabilities/cmd/script/config.go b/harnesses/rpc-capabilities/cmd/script/config.go index b8df721c..8af3e2ce 100644 --- a/harnesses/rpc-capabilities/cmd/script/config.go +++ b/harnesses/rpc-capabilities/cmd/script/config.go @@ -1629,6 +1629,48 @@ func chains() []Chain { {Slug: "ngd2", Name: "NGD (2)", URL: envDefault("RPC_URL_NEO_NGD2", "https://n3seed2.ngd.network:10332")}, }, }, + // 2026-08-27 wave-10. Litecoin — Tatum JSON-RPC (getblockcount) + BlockCypher REST + LitecoinSpace Esplora REST. ~150 s/block. + { + Slug: "litecoin", + Name: "Litecoin", + Kind: "litecoin", + Providers: []Provider{ + {Slug: "tatum", Name: "Tatum", URL: envDefault("RPC_URL_LTC_TATUM", "https://litecoin-mainnet.gateway.tatum.io")}, + {Slug: "blockcypher", Name: "BlockCypher", URL: envDefault("RPC_URL_LTC_BLOCKCYPHER", "https://api.blockcypher.com/v1/ltc/main")}, + {Slug: "litecoinspace", Name: "LitecoinSpace", URL: envDefault("RPC_URL_LTC_LITECOINSPACE", "https://litecoinspace.org/api/blocks/tip/height")}, + }, + }, + // 2026-08-27 wave-10. Babylon — Cosmos SDK, Tendermint /status. PublicNode + Polkachu + LavenderFive. + { + Slug: "babylon", + Name: "Babylon", + Kind: "cosmos", + Providers: []Provider{ + {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_BABYLON_PUBLICNODE", "https://babylon-rpc.publicnode.com")}, + {Slug: "polkachu", Name: "Polkachu", URL: envDefault("RPC_URL_BABYLON_POLKACHU", "https://babylon-rpc.polkachu.com")}, + {Slug: "lavenderfive", Name: "LavenderFive", URL: envDefault("RPC_URL_BABYLON_LAVENDERFIVE", "https://rpc.lavenderfive.com:443/babylon")}, + }, + }, + // 2026-08-27 wave-10. Acala — Polkadot parachain, chain_getHeader. Dwellir + Acala official nodes. + { + Slug: "acala", + Name: "Acala", + Kind: "polkadot", + Providers: []Provider{ + {Slug: "dwellir", Name: "Dwellir", URL: envDefault("RPC_URL_ACALA_DWELLIR", "https://acala-rpc.n.dwellir.com")}, + {Slug: "acala-rpc0", Name: "Acala (RPC-0)", URL: envDefault("RPC_URL_ACALA_RPC0", "https://acala-rpc-0.aca-api.network")}, + {Slug: "acala-rpc2", Name: "Acala (RPC-2)", URL: envDefault("RPC_URL_ACALA_RPC2", "https://acala-rpc-2.aca-api.network")}, + }, + }, + // 2026-08-27 wave-10. Interlay — Polkadot parachain, chain_getHeader. Only official node has live HTTP RPC. + { + Slug: "interlay", + Name: "Interlay", + Kind: "polkadot", + Providers: []Provider{ + {Slug: "interlay-official", Name: "Interlay", URL: envDefault("RPC_URL_INTERLAY_OFFICIAL", "https://api.interlay.io/parachain")}, + }, + }, // 2026-08-27 wave-10. Zcash — getblockcount JSON-RPC (Tatum zcashd + Tatum Zebra) + Blockchair REST GET. ~75 s/block. { Slug: "zcash", diff --git a/harnesses/rpc-capabilities/cmd/script/probe.go b/harnesses/rpc-capabilities/cmd/script/probe.go index fbbc9c48..6a2569e2 100644 --- a/harnesses/rpc-capabilities/cmd/script/probe.go +++ b/harnesses/rpc-capabilities/cmd/script/probe.go @@ -98,6 +98,9 @@ const ( // bitcoinCashStaleBlockGap: BCH targets one block every ~600 s, // so 2 blocks ≈ 20 min gives tolerance for normal variance. bitcoinCashStaleBlockGap uint64 = 2 + // litecoinStaleBlockGap: Litecoin targets one block every ~150 s, + // so 4 blocks ≈ 10 min. + litecoinStaleBlockGap uint64 = 4 // veChainStaleBlockGap: VeChain produces one block every ~10 s, // so 30 blocks ≈ 5 min. veChainStaleBlockGap uint64 = 30 @@ -309,6 +312,14 @@ func probeOne(ctx context.Context, c Chain, p Provider) { } else { block, result, latency, err = callBCHNodeInfo(probeCtx, p.URL) } + case "litecoin": + if strings.Contains(p.URL, "blockcypher.com") { + block, result, latency, err = callBlockCypherLTC(probeCtx, p.URL) + } else if strings.Contains(p.URL, "litecoinspace.org") { + block, result, latency, err = callLitecoinSpaceAPI(probeCtx, p.URL) + } else { + block, result, latency, err = callNeoBlockCount(probeCtx, p.URL) + } case "tezos": block, result, latency, err = callTezosBlock(probeCtx, p.URL) case "antelope": @@ -364,6 +375,8 @@ func probeOne(ctx context.Context, c Chain, p Provider) { gap = dogecoinStaleBlockGap case "bitcoin-cash": gap = bitcoinCashStaleBlockGap + case "litecoin": + gap = litecoinStaleBlockGap case "tezos": gap = tezosStaleBlockGap case "antelope": @@ -393,7 +406,7 @@ func probeOne(ctx context.Context, c Chain, p Provider) { case "solana", "polkadot", "cosmos", "starknet", "stellar", "sui", "aptos", "xrpl", "algorand", "gram", "near", "flow", "hedera", "ckb", "multiversx", "neo", - "tezos", "antelope", "waves", "vechain", "dogecoin", "zcash", "bitcoin-cash": + "tezos", "antelope", "waves", "vechain", "dogecoin", "zcash", "bitcoin-cash", "litecoin": // no consensus participation default: if result == "ok" || result == "stale" { @@ -1685,3 +1698,77 @@ func callBlockCypherDoge(ctx context.Context, url string) (height uint64, result } return body.Height, "ok", latencyMs, nil } + +// callBlockCypherLTC probes the BlockCypher REST API for Litecoin mainnet. +// GET https://api.blockcypher.com/v1/ltc/main — reads the `height` field. +func callBlockCypherLTC(ctx context.Context, url string) (height uint64, result string, latencyMs float64, err error) { + req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) + req.Header.Set("User-Agent", "OpenChainBench/1.0 (+https://openchainbench.com)") + client := &http.Client{Timeout: probeTimeout} + + start := time.Now() + resp, err := client.Do(req) + latencyMs = float64(time.Since(start).Nanoseconds()) / 1e6 + + if err != nil { + if ctx.Err() != nil || strings.Contains(err.Error(), "deadline exceeded") || strings.Contains(err.Error(), "Timeout") { + return 0, "timeout", latencyMs, err + } + return 0, "http_err", latencyMs, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + _, _ = io.Copy(io.Discard, resp.Body) + return 0, "http_err", latencyMs, fmt.Errorf("status %d", resp.StatusCode) + } + + var body struct { + Height uint64 `json:"height"` + } + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + return 0, "http_err", latencyMs, err + } + if body.Height == 0 { + return 0, "jsonrpc_err", latencyMs, fmt.Errorf("blockcypher ltc: height=0") + } + return body.Height, "ok", latencyMs, nil +} + +// callLitecoinSpaceAPI probes the LitecoinSpace Esplora API. +// GET https://litecoinspace.org/api/blocks/tip/height — returns a plain ASCII integer. +func callLitecoinSpaceAPI(ctx context.Context, url string) (height uint64, result string, latencyMs float64, err error) { + req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) + req.Header.Set("User-Agent", "OpenChainBench/1.0 (+https://openchainbench.com)") + client := &http.Client{Timeout: probeTimeout} + + start := time.Now() + resp, err := client.Do(req) + latencyMs = float64(time.Since(start).Nanoseconds()) / 1e6 + + if err != nil { + if ctx.Err() != nil || strings.Contains(err.Error(), "deadline exceeded") || strings.Contains(err.Error(), "Timeout") { + return 0, "timeout", latencyMs, err + } + return 0, "http_err", latencyMs, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + _, _ = io.Copy(io.Discard, resp.Body) + return 0, "http_err", latencyMs, fmt.Errorf("status %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, "http_err", latencyMs, err + } + var h uint64 + if _, err := fmt.Sscanf(strings.TrimSpace(string(body)), "%d", &h); err != nil { + return 0, "jsonrpc_err", latencyMs, fmt.Errorf("litecoinspace: non-integer body: %s", string(body)) + } + if h == 0 { + return 0, "jsonrpc_err", latencyMs, fmt.Errorf("litecoinspace: height=0") + } + return h, "ok", latencyMs, nil +} diff --git a/infrastructure/monitoring/prometheus/prometheus.yml b/infrastructure/monitoring/prometheus/prometheus.yml index 78a3f278..74e9c0dd 100644 --- a/infrastructure/monitoring/prometheus/prometheus.yml +++ b/infrastructure/monitoring/prometheus/prometheus.yml @@ -208,8 +208,7 @@ scrape_configs: honor_labels: true static_configs: - targets: - - 'rpc-keyed-us.railway.internal:2112' # us-east - - 'rpc-keyed-us-west.railway.internal:2112' # us-west + - 'rpc-keyed-us.railway.internal:2112' # us-east only; eu-west+sgp run on VPS labels: benchmark: rpc-keyed-latency metrics_path: /metrics diff --git a/public/logos/acala.svg b/public/logos/acala.svg new file mode 100644 index 00000000..0b880d9f --- /dev/null +++ b/public/logos/acala.svg @@ -0,0 +1,4 @@ + + + ACA + diff --git a/public/logos/babylon.svg b/public/logos/babylon.svg new file mode 100644 index 00000000..f511dd89 --- /dev/null +++ b/public/logos/babylon.svg @@ -0,0 +1,4 @@ + + + BBN + diff --git a/public/logos/interlay.svg b/public/logos/interlay.svg new file mode 100644 index 00000000..205fa451 --- /dev/null +++ b/public/logos/interlay.svg @@ -0,0 +1,4 @@ + + + INTR + diff --git a/public/logos/litecoinspace.svg b/public/logos/litecoinspace.svg new file mode 100644 index 00000000..d54eecc0 --- /dev/null +++ b/public/logos/litecoinspace.svg @@ -0,0 +1,4 @@ + + + LTC + diff --git a/scripts/sitemap-smoke.mjs b/scripts/sitemap-smoke.mjs index b39a75bc..1ef9699d 100644 --- a/scripts/sitemap-smoke.mjs +++ b/scripts/sitemap-smoke.mjs @@ -26,10 +26,16 @@ const base = process.argv[2]; if (!base) { - console.error("Usage: node scripts/sitemap-smoke.mjs "); + console.error("Usage: node scripts/sitemap-smoke.mjs [check-host]"); process.exit(2); } +// Optional second arg: host to check individual page URLs against. +// When base is a Vercel preview URL (which injects noindex on every page +// by design), pass the prod domain here so page checks run against the +// real indexed host while the sitemap is still fetched fresh from base. +const checkHost = process.argv[3] ? new URL(process.argv[3]).origin : null; + const CONCURRENCY = Number(process.env.SMOKE_CONCURRENCY ?? 8); const TIMEOUT_MS = Number(process.env.SMOKE_TIMEOUT_MS ?? 20000); const SKIP_REGEX = process.env.SMOKE_SKIP_REGEX @@ -87,10 +93,12 @@ if (locs.length === 0) { process.exit(1); } -// Rewrite each URL's host to match the target base, so a smoke test -// against a Vercel preview URL still exercises the right deployment -// rather than hitting prod. -const targetHost = new URL(base).origin; +// Rewrite each URL's host: use checkHost when provided (prod domain so +// preview-URL noindex doesn't false-fail), otherwise same host as base. +const targetHost = checkHost ?? new URL(base).origin; +if (checkHost) { + console.log(`[smoke] checking pages against ${targetHost}`); +} const urls = locs.map((u) => { try { const parsed = new URL(u); diff --git a/scripts/test-gmx-funding.ts b/scripts/test-gmx-funding.ts new file mode 100644 index 00000000..5e2c33db --- /dev/null +++ b/scripts/test-gmx-funding.ts @@ -0,0 +1,130 @@ +/** + * Test script: validates GMX v2 funding rate keys + DataStore reads. + * Run: npx ts-node --transpile-only scripts/test-gmx-funding.ts + */ + +import { keccak256 } from "js-sha3"; +import * as https from "https"; + +const ARB_RPC = "https://arb1.arbitrum.io/rpc"; +const GMX_DATASTORE = "0xFD70de6b91282D8017aA4E741e9Ae325CAb992d8"; +const USDC_ARB = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; + +const GMX_MARKETS: Record = { + "0x47c031236e19d024b42f8AE6780E44A573170703": "BTC", + "0x70d95587d40A2caf56bd97485aB3Eec10Bee6336": "ETH", + "0x09400D9DB990D5ed3f35D7be61DfAEB900Af03C9": "SOL", + "0xC25cEf6061Cf5dE5eb761b50E4743c1F5D7E5407": "ARB", + "0x7f1fa204bb700853D36994DA19F830b6Ad18d232": "LINK", + "0x6853EA96FF216fAb11D2d930CE3C508556A4bdc4": "DOGE", + "0xD9535bB5f58A1a75032416F2dFe7880C30575a41": "XRP", +}; + +const GMX_MARKET_LONG_TOKEN: Record = { + "0x47c031236e19d024b42f8AE6780E44A573170703": "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f", + "0x70d95587d40A2caf56bd97485aB3Eec10Bee6336": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + "0x09400D9DB990D5ed3f35D7be61DfAEB900Af03C9": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + "0xC25cEf6061Cf5dE5eb761b50E4743c1F5D7E5407": "0x912CE59144191C1204E64559FE8253a0e49E6548", + "0x7f1fa204bb700853D36994DA19F830b6Ad18d232": "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4", + "0x6853EA96FF216fAb11D2d930CE3C508556A4bdc4": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + "0xD9535bB5f58A1a75032416F2dFe7880C30575a41": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", +}; + +function jsonRpc(body: object): Promise<{ result?: string; error?: unknown }> { + return new Promise((resolve, reject) => { + const payload = JSON.stringify(body); + const url = new URL(ARB_RPC); + const req = https.request( + { + hostname: url.hostname, + path: url.pathname, + method: "POST", + headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) }, + }, + (res) => { + let data = ""; + res.on("data", (chunk: Buffer) => (data += chunk)); + res.on("end", () => resolve(JSON.parse(data))); + } + ); + req.on("error", reject); + req.write(payload); + req.end(); + }); +} + +async function callGetUint(storageKey: string): Promise { + const keyHex = storageKey.replace("0x", "").padStart(64, "0"); + const calldata = "0xbd02d0f5" + keyHex; // selector for getUint(bytes32) + const res = await jsonRpc({ + jsonrpc: "2.0", + id: 1, + method: "eth_call", + params: [{ to: GMX_DATASTORE, data: calldata }, "latest"], + }); + if (!res.result || res.result === "0x") return BigInt(0); + return BigInt(res.result); +} + +function gmxStringBase(str: string): string { + const enc = new Uint8Array(96); + enc[31] = 0x20; + enc[63] = str.length; + Buffer.from(str, "utf8").copy(Buffer.from(enc.buffer), 64); + return keccak256(Array.from(enc)); +} + +function computeFundingFactorKey(marketAddr: string): string { + const base = gmxStringBase("FUNDING_FACTOR"); + const enc = new Uint8Array(64); + Buffer.from(base, "hex").copy(Buffer.from(enc.buffer), 0); + Buffer.from(marketAddr.replace("0x", "").toLowerCase(), "hex").copy(Buffer.from(enc.buffer), 44); + return "0x" + keccak256(Array.from(enc)); +} + +function computeOpenInterestKey(marketAddr: string, collateralToken: string, isLong: boolean): string { + const base = gmxStringBase("OPEN_INTEREST"); + const enc = new Uint8Array(128); + Buffer.from(base, "hex").copy(Buffer.from(enc.buffer), 0); + Buffer.from(marketAddr.replace("0x", "").toLowerCase(), "hex").copy(Buffer.from(enc.buffer), 44); + Buffer.from(collateralToken.replace("0x", "").toLowerCase(), "hex").copy(Buffer.from(enc.buffer), 76); + enc[127] = isLong ? 1 : 0; + return "0x" + keccak256(Array.from(enc)); +} + +async function main() { + console.log("=== GMX v2 Funding Rate Test ==="); + console.log("DataStore:", GMX_DATASTORE, "\n"); + + for (const [marketAddr, coin] of Object.entries(GMX_MARKETS)) { + const longToken = GMX_MARKET_LONG_TOKEN[marketAddr]; + if (!longToken) continue; + + const [ffRaw, longsOILong, longsOIShort, shortsOIShort, shortsOILong] = await Promise.all([ + callGetUint(computeFundingFactorKey(marketAddr)), + callGetUint(computeOpenInterestKey(marketAddr, longToken, true)), + callGetUint(computeOpenInterestKey(marketAddr, USDC_ARB, true)), + callGetUint(computeOpenInterestKey(marketAddr, USDC_ARB, false)), + callGetUint(computeOpenInterestKey(marketAddr, longToken, false)), + ]); + + const longsOI = longsOILong + longsOIShort; + const shortsOI = shortsOIShort + shortsOILong; + const totalOI = longsOI + shortsOI; + + if (totalOI === BigInt(0) || ffRaw === BigInt(0)) { + console.log(`${coin}: OI or fundingFactor = 0 (no data)`); + continue; + } + + const imbalance = longsOI > shortsOI ? longsOI - shortsOI : shortsOI - longsOI; + const rateScaled = ffRaw * imbalance / totalOI; + const rate = Number(rateScaled) / 1e30; + const inRange = rate > 1e-12 && rate < 1e-6; + + console.log(`${coin}: rate=${rate.toExponential(3)}/sec = ${(rate * 3600 * 100).toFixed(6)}%/hr [${inRange ? "OK" : "OUT OF RANGE"}]`); + console.log(` fundingFactor=${ffRaw} longsOI=${longsOI} shortsOI=${shortsOI}`); + } +} + +main().catch(console.error); diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index 4023fed9..cd724592 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -18,7 +18,7 @@ const HL_TAKER_FALLBACK = 0.00035; // GMX v2 on Arbitrum const ARB_RPC = "https://arb1.arbitrum.io/rpc"; -const GMX_DATASTORE = "0xFD70de6b91282D8588DEF3023E68e548b6898A2b"; +const GMX_DATASTORE = "0xFD70de6b91282D8017aA4E741e9Ae325CAb992d8"; const GMX_MARKETS: Record = { "0x47c031236e19d024b42f8AE6780E44A573170703": "BTC", "0x70d95587d40A2caf56bd97485aB3Eec10Bee6336": "ETH", @@ -31,6 +31,19 @@ const GMX_MARKETS: Record = { // GMX typical utilization assumption for base borrowing factor const GMX_AVG_UTILIZATION = 0.4; +const USDC_ARB = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; +// Primary long collateral per GMX v2 market on Arbitrum. +// Real markets use the native token; synthetic markets use WETH. +const GMX_MARKET_LONG_TOKEN: Record = { + "0x47c031236e19d024b42f8AE6780E44A573170703": "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f", // BTC → WBTC + "0x70d95587d40A2caf56bd97485aB3Eec10Bee6336": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", // ETH → WETH + "0x09400D9DB990D5ed3f35D7be61DfAEB900Af03C9": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", // SOL → WETH (synthetic) + "0xC25cEf6061Cf5dE5eb761b50E4743c1F5D7E5407": "0x912CE59144191C1204E64559FE8253a0e49E6548", // ARB → ARB + "0x7f1fa204bb700853D36994DA19F830b6Ad18d232": "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4", // LINK → LINK + "0x6853EA96FF216fAb11D2d930CE3C508556A4bdc4": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", // DOGE → WETH (synthetic) + "0xD9535bB5f58A1a75032416F2dFe7880C30575a41": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", // XRP → WETH (synthetic) +}; + const WALLET_RE = /^0x[0-9a-fA-F]{40}$/; const DYDX_ADDRESS_RE = /^dydx1[a-z0-9]{38}$/; const MAX_DISPLAY_FILLS = 50; @@ -167,7 +180,8 @@ type HlWalletData = { type GainsWalletData = { events: number; - feesUsdc: number; + feesUsdc: number; // pure taker fee only (uiRealizedPnlData) + vaultFeesUsdc: number; // OI imbalance vault surcharge (tradeFeesData - uiRealizedPnlData) fundingFeesUsdc: number; fundingEstimated: boolean; borrowingFeesUsdc: number; @@ -179,9 +193,11 @@ type GainsWalletData = { pair: string; action: string; notional: number; - tradingFee: number; + tradingFee: number; // taker fee only + vaultFee: number; // OI vault surcharge fundingFee: number; borrowingFee: number; + equivFee?: number; // equivalent fee on the other venue pnl_net: number; }>; }; @@ -326,10 +342,10 @@ async function fetchGainsFeeRates(): Promise<{ borrowPerSecPerCoin[p.from] = parseFloat(v2Borrow) / GAINS_BORROW_PRECISION; } - // Funding rate per second (absolute value — longs and shorts may face same magnitude) + // Signed funding rate: positive = longs pay shorts, negative = shorts pay longs const fundingRate = fundingPairData[i]?.lastFundingRatePerSecondP; if (fundingRate) { - fundingPerSecPerCoin[p.from] = Math.abs(parseFloat(fundingRate)) / GAINS_FUNDING_PRECISION; + fundingPerSecPerCoin[p.from] = parseFloat(fundingRate) / GAINS_FUNDING_PRECISION; } } @@ -400,8 +416,9 @@ async function fetchDydxCarryRates(): Promise { for (const [market, info] of Object.entries(data.markets ?? {})) { // "BTC-USD" → "BTC", "ETH-USD-PERP" → "ETH" const coin = market.replace(/-USD.*/, ""); - const rate = Math.abs(parseFloat(info.nextFundingRate ?? "0")) / 3600; // per hour → per sec - if (rate > 0) fundingPerSecPerCoin[coin] = rate; + // Signed: positive = longs pay shorts, negative = shorts pay longs + const rate = parseFloat(info.nextFundingRate ?? "0") / 3600; + if (rate !== 0) fundingPerSecPerCoin[coin] = rate; } const result: CarryRates = { fundingPerSecPerCoin, borrowPerSecPerCoin: {}, ts: Date.now() }; carryRateCache["dydx"] = result; @@ -480,6 +497,37 @@ function computeBorrowingFactorKey(marketAddr: string, isLong: boolean): string return "0x" + keccak256(Array.from(enc)); } +// keccak256(abi.encode(string)) — shared by all GMX DataStore key bases +function gmxStringBase(str: string): string { + const enc = new Uint8Array(96); + enc[31] = 0x20; // offset = 32 + enc[63] = str.length; + Buffer.from(str, "utf8").copy(Buffer.from(enc.buffer), 64); + return keccak256(Array.from(enc)); +} + +// keccak256(abi.encode(FUNDING_FACTOR, market)) +function computeFundingFactorKey(marketAddr: string): string { + const base = gmxStringBase("FUNDING_FACTOR"); + // abi.encode(bytes32, address) = 64 bytes + const enc = new Uint8Array(64); + Buffer.from(base, "hex").copy(Buffer.from(enc.buffer), 0); + Buffer.from(marketAddr.replace("0x", "").toLowerCase(), "hex").copy(Buffer.from(enc.buffer), 44); + return "0x" + keccak256(Array.from(enc)); +} + +// keccak256(abi.encode(OPEN_INTEREST, market, collateralToken, isLong)) +function computeOpenInterestKey(marketAddr: string, collateralToken: string, isLong: boolean): string { + const base = gmxStringBase("OPEN_INTEREST"); + // abi.encode(bytes32, address, address, bool) = 128 bytes + const enc = new Uint8Array(128); + Buffer.from(base, "hex").copy(Buffer.from(enc.buffer), 0); + Buffer.from(marketAddr.replace("0x", "").toLowerCase(), "hex").copy(Buffer.from(enc.buffer), 44); + Buffer.from(collateralToken.replace("0x", "").toLowerCase(), "hex").copy(Buffer.from(enc.buffer), 76); + enc[127] = isLong ? 1 : 0; + return "0x" + keccak256(Array.from(enc)); +} + async function ethCallGetUint(contract: string, storageKey: string): Promise { // getUint(bytes32): selector = first 4 bytes of keccak256("getUint(bytes32)") const selectorHash = keccak256(Array.from(Buffer.from("getUint(bytes32)", "utf8"))); @@ -558,7 +606,47 @@ async function fetchGmxCarryRates(): Promise { if (!borrowPerSecPerCoin[coin]) borrowPerSecPerCoin[coin] = rate; } - const result: CarryRates = { borrowPerSecPerCoin, fundingPerSecPerCoin: {}, ts: Date.now() }; + // Funding rates: 3 DataStore reads per market (fundingFactor + longsOI + shortsOI) + const fundingPerSecPerCoin: Record = {}; + const fundingReads = await Promise.allSettled( + Object.entries(GMX_MARKETS).map(async ([marketAddr, coin]) => { + const longToken = GMX_MARKET_LONG_TOKEN[marketAddr]; + if (!longToken) return null; + + const [fundingFactorRaw, longsOILong, longsOIShort, shortsOIShort, shortsOILong] = await Promise.all([ + ethCallGetUint(GMX_DATASTORE, computeFundingFactorKey(marketAddr)), + // OI is tracked per collateral token; sum both to get total side OI + ethCallGetUint(GMX_DATASTORE, computeOpenInterestKey(marketAddr, longToken, true)), + ethCallGetUint(GMX_DATASTORE, computeOpenInterestKey(marketAddr, USDC_ARB, true)), + ethCallGetUint(GMX_DATASTORE, computeOpenInterestKey(marketAddr, USDC_ARB, false)), + ethCallGetUint(GMX_DATASTORE, computeOpenInterestKey(marketAddr, longToken, false)), + ]); + + const longsOI = longsOILong + longsOIShort; + const shortsOI = shortsOIShort + shortsOILong; + const totalOI = longsOI + shortsOI; + + if (totalOI === BigInt(0) || fundingFactorRaw === BigInt(0)) return { coin, rate: 0 }; + + const imbalance = longsOI > shortsOI ? longsOI - shortsOI : shortsOI - longsOI; + // rate = fundingFactor × (imbalance / totalOI) / 1e30 + const rateScaled = fundingFactorRaw * imbalance / totalOI; + const rate = Number(rateScaled) / 1e30; + + return { coin, rate }; + }) + ); + + for (const r of fundingReads) { + if (r.status !== "fulfilled" || !r.value) continue; + const { coin, rate } = r.value; + // Sanity check: GMX funding should be between 1e-12 and 1e-6 /sec + if (rate > 1e-12 && rate < 1e-6) { + fundingPerSecPerCoin[coin] = rate; + } + } + + const result: CarryRates = { borrowPerSecPerCoin, fundingPerSecPerCoin, ts: Date.now() }; carryRateCache["gmx-v2"] = { ...result, ts: Date.now() }; return result; } catch { @@ -639,6 +727,28 @@ async function resolveRate(slug: string): Promise<{ rate: number; note: string; return { rate: 0.0005, note: "Documented rate", rateIsLive: false }; } +type HlOpenPos = { + coin: string; + szi: string; + entryPx: string; + positionValue: string; +}; + +async function fetchHlOpenPositions(wallet: string): Promise { + const res = await fetch(HL_API, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type: "clearinghouseState", user: wallet }), + signal: AbortSignal.timeout(8000), + }); + const data = (await res.json()) as { + assetPositions?: Array<{ position: HlOpenPos }>; + }; + return (data.assetPositions ?? []) + .map((p) => p.position) + .filter((p) => Math.abs(parseFloat(p.szi)) > 0.000001); +} + async function fetchHlFills(wallet: string): Promise { const res = await fetch(HL_API, { method: "POST", @@ -970,6 +1080,35 @@ function reconstructHlPositions(fills: HlFill[], cutoffMs: number): PositionSlic return slices; } +// Inject currently-open HL positions not captured by the 2000-fill API cap. +// For each open position not already in slices (keyed by coin:side), add a slice +// from cutoffMs to now using the current positionValue as notional. +function augmentWithHlOpenPositions( + slices: PositionSlice[], + openPositions: HlOpenPos[], + cutoffMs: number +): PositionSlice[] { + const now = Date.now(); + // Coins already tracked as still-open in the fill reconstruction + const tracked = new Set(); + for (const s of slices) { + if (s.closeMs >= now - 5 * 60 * 1000) { + tracked.add(`${s.coin}:${s.isLong ? "L" : "S"}`); + } + } + for (const pos of openPositions) { + const sz = parseFloat(pos.szi); + if (!sz) continue; + const isLong = sz > 0; + const key = `${pos.coin}:${isLong ? "L" : "S"}`; + if (tracked.has(key)) continue; + const notionalUsd = Math.abs(parseFloat(pos.positionValue)); + if (notionalUsd < 1) continue; + slices.push({ coin: pos.coin, notionalUsd, openMs: cutoffMs, closeMs: now, isLong }); + } + return slices; +} + function reconstructGainsPositions(trades: GainsApiTrade[], cutoffMs: number): PositionSlice[] { const OPEN_ACTIONS = new Set(["MarketOpened", "LimitOrderExecuted"]); const CLOSE_ACTIONS = new Set(["TradeClosedMarket", "TradeClosedTP", "TradeClosedSL", "TradeClosedLIQ"]); @@ -1123,9 +1262,10 @@ function computeHlFunding( const rates = (history.get(pos.coin) ?? []).filter( (r) => r.time >= pos.openMs && r.time <= pos.closeMs ); - // Each HL funding entry = one 8h interval. Rate is a fraction applied to notional. + // Each HL funding entry = one 8h interval. Rate > 0 = longs pay; < 0 = shorts pay. for (const r of rates) { - total += pos.notionalUsd * Math.abs(r.rate); + const cost = pos.isLong ? r.rate : -r.rate; + total += pos.notionalUsd * Math.max(0, cost); } } return total; @@ -1159,7 +1299,9 @@ function estimateGainsFundingFees( const rate = fundingPerSecPerCoin[pos.coin]; if (!rate) continue; const durationSec = Math.max(0, (pos.closeMs - pos.openMs) / 1000); - total += pos.notionalUsd * rate * durationSec; + // positive rate = longs pay; negative rate = shorts pay + const effectiveRate = pos.isLong ? Math.max(0, rate) : Math.max(0, -rate); + total += pos.notionalUsd * effectiveRate * durationSec; } return total; } @@ -1173,7 +1315,10 @@ function estimateCarryFees( for (const pos of positions) { const durationSec = Math.max(0, (pos.closeMs - pos.openMs) / 1000); borrowFees += pos.notionalUsd * (rates.borrowPerSecPerCoin[pos.coin] ?? 0) * durationSec; - fundingFees += pos.notionalUsd * (rates.fundingPerSecPerCoin[pos.coin] ?? 0) * durationSec; + const fundingRate = rates.fundingPerSecPerCoin[pos.coin] ?? 0; + // positive rate = longs pay; negative rate = shorts pay + const fundingCost = pos.isLong ? Math.max(0, fundingRate) : Math.max(0, -fundingRate); + fundingFees += pos.notionalUsd * fundingCost * durationSec; } return { borrowFees, fundingFees }; } @@ -1275,6 +1420,7 @@ export async function GET(req: Request) { let hlFillsData: HlFill[] = []; let hlFundingData: HlFundingEvent[] = []; + let hlOpenPositions: HlOpenPos[] = []; let gainsTradesData: GainsApiTrade[] = []; let gmxWalletData: GmxWalletData | null = null; let dydxWalletData: DydxWalletData | null = null; @@ -1289,7 +1435,13 @@ export async function GET(req: Request) { }), fetchHlFunding(wallet, cutoffMs).then((f) => { hlFundingData = f; - }) + }), + // clearinghouseState gives currently-open positions whose fill may be outside + // the 2000-fill API cap — without this, long-held positions are invisible to + // the carry projection even though they generate real HL funding payments. + fetchHlOpenPositions(wallet).then((p) => { + hlOpenPositions = p; + }).catch(() => {}) ); } if (venueA === "gains" || venueB === "gains") { @@ -1361,23 +1513,35 @@ export async function GET(req: Request) { } else if (fetchEvmWallet && slug === "gains") { const CLOSE_ACTIONS = new Set(["TradeClosedMarket", "TradeClosedTP", "TradeClosedSL", "TradeClosedLIQ"]); const usdcTrades = gainsTradesData.filter((t) => t.collateralIndex === 3); + const otherSlug = slug === venueA ? venueB : venueA; + const otherRate = slug === venueA ? rateB : rateA; let feesUsdc = 0; + let vaultFeesUsdc = 0; let fundingFeesUsdc = 0; let borrowingFeesUsdc = 0; let notionalUsd = 0; const recentTrades: GainsWalletData["recentTrades"] = []; for (const t of usdcTrades) { - const tradingFee = t.meta?.tradeFeesData?.realizedTradingFeesCollateral ?? 0; + // uiRealizedPnlData = pure taker fee; tradeFeesData = taker + OI vault surcharge + const takerFee = t.meta?.uiRealizedPnlData?.realizedTradingFeesCollateral + ?? t.meta?.tradeFeesData?.realizedTradingFeesCollateral ?? 0; + const totalTradingFee = t.meta?.tradeFeesData?.realizedTradingFeesCollateral ?? takerFee; + const vaultFee = Math.max(0, totalTradingFee - takerFee); const isClose = CLOSE_ACTIONS.has(t.action); const fundingFee = isClose ? (t.meta?.uiRealizedPnlData?.realizedFundingFeesCollateral ?? 0) : 0; const borrowingFee = isClose ? (t.meta?.uiRealizedPnlData?.realizedNewBorrowingFeesCollateral ?? 0) : 0; - feesUsdc += tradingFee; + feesUsdc += takerFee; + vaultFeesUsdc += vaultFee; fundingFeesUsdc += fundingFee; borrowingFeesUsdc += borrowingFee; - notionalUsd += t.size * t.leverage; + const tradeNotional = t.size * t.leverage; + notionalUsd += tradeNotional; if (recentTrades.length < 50) { - recentTrades.push({ date: t.date, pair: t.pair, action: t.action, notional: t.size * t.leverage, tradingFee, fundingFee, borrowingFee, pnl_net: t.pnl_net }); + const equivFee = otherSlug === "hyperliquid" + ? tradeNotional * (gainsData.perSide[t.pair.split("/")[0]] ?? otherRate) + : tradeNotional * otherRate; + recentTrades.push({ date: t.date, pair: t.pair, action: t.action, notional: tradeNotional, tradingFee: takerFee, vaultFee, fundingFee, borrowingFee, equivFee, pnl_net: t.pnl_net }); } } @@ -1393,16 +1557,17 @@ export async function GET(req: Request) { } } - const netCostUsdc = feesUsdc + fundingFeesUsdc + borrowingFeesUsdc; + const netCostUsdc = feesUsdc + vaultFeesUsdc + fundingFeesUsdc + borrowingFeesUsdc; walletData = { events: usdcTrades.length, feesUsdc, + vaultFeesUsdc, fundingFeesUsdc, fundingEstimated, borrowingFeesUsdc, netCostUsdc, positionSizeUsdc: notionalUsd, - avgFeeRateBps: notionalUsd > 0 ? (netCostUsdc / notionalUsd) * 10000 : 0, + avgFeeRateBps: notionalUsd > 0 ? (feesUsdc / notionalUsd) * 10000 : 0, recentTrades, } satisfies GainsWalletData; } else if (fetchEvmWallet && slug === "gmx-v2" && gmxWalletData) { @@ -1445,8 +1610,15 @@ export async function GET(req: Request) { const aFunding = hlW.fundingUsd; const aNetCost = aFees - aFunding; - // Estimate Gains carry (borrow + funding) by reconstructing HL positions - const hlPositions = reconstructHlPositions(hlFillsData, cutoffMs); + // Estimate Gains carry (borrow + funding) by reconstructing HL positions. + // augmentWithHlOpenPositions fills in positions whose open fill is older than the + // 2000-fill API cap — they still generate real HL funding but are invisible to + // fill-only reconstruction. + const hlPositions = augmentWithHlOpenPositions( + reconstructHlPositions(hlFillsData, cutoffMs), + hlOpenPositions, + cutoffMs + ); const gainsBorrow = estimateGainsBorrowFees(hlPositions, gainsData.borrowPerSecPerCoin, gainsData.avgBorrowPerSec); const gainsFunding = estimateGainsFundingFees(hlPositions, gainsData.fundingPerSecPerCoin); const bEquiv = takerEquiv + gainsBorrow + gainsFunding; @@ -1482,7 +1654,11 @@ export async function GET(req: Request) { // Reconstruct positions from venueA for carry projection let positions: PositionSlice[] = []; if (venueA === "hyperliquid" && hlFillsData.length > 0) { - positions = reconstructHlPositions(hlFillsData, cutoffMs); + positions = augmentWithHlOpenPositions( + reconstructHlPositions(hlFillsData, cutoffMs), + hlOpenPositions, + cutoffMs + ); } else if (venueA === "gains" && gainsTradesData.length > 0) { positions = reconstructGainsPositions( gainsTradesData.filter((t) => t.collateralIndex === 3), @@ -1578,7 +1754,11 @@ export async function GET(req: Request) { const bFunding = hlW.fundingUsd; const bNetCost = bFees - bFunding; - const hlPositions = reconstructHlPositions(hlFillsData, cutoffMs); + const hlPositions = augmentWithHlOpenPositions( + reconstructHlPositions(hlFillsData, cutoffMs), + hlOpenPositions, + cutoffMs + ); const gainsBorrow = estimateGainsBorrowFees(hlPositions, gainsData.borrowPerSecPerCoin, gainsData.avgBorrowPerSec); const gainsFunding = estimateGainsFundingFees(hlPositions, gainsData.fundingPerSecPerCoin); const aEquiv = takerEquiv + gainsBorrow + gainsFunding; @@ -1614,7 +1794,11 @@ export async function GET(req: Request) { // Reconstruct positions from venueB let positions: PositionSlice[] = []; if (venueB === "hyperliquid" && hlFillsData.length > 0) { - positions = reconstructHlPositions(hlFillsData, cutoffMs); + positions = augmentWithHlOpenPositions( + reconstructHlPositions(hlFillsData, cutoffMs), + hlOpenPositions, + cutoffMs + ); } else if (venueB === "gains" && gainsTradesData.length > 0) { positions = reconstructGainsPositions( gainsTradesData.filter((t) => t.collateralIndex === 3), diff --git a/src/app/fee-compare/page.tsx b/src/app/fee-compare/page.tsx index ad2fed51..3cc0f946 100644 --- a/src/app/fee-compare/page.tsx +++ b/src/app/fee-compare/page.tsx @@ -9,7 +9,23 @@ export const metadata: Metadata = pageMetadata({ "Compare taker fees between any two perp DEXs. Paste a wallet to see what was actually paid on Hyperliquid or Gains and what it would have cost elsewhere. Live on-chain data, no API key.", }); -export default function FeeComparePage() { +const VALID_VENUES = new Set(["hyperliquid", "gains", "dydx", "gmx-v2", "paradex", "edgex"]); + +export default async function FeeComparePage({ + searchParams, +}: { + searchParams: Promise>; +}) { + const params = await searchParams; + + const rawA = params.venueA ?? "hyperliquid"; + const rawB = params.venueB ?? "gains"; + const initialVenueA = VALID_VENUES.has(rawA) ? rawA : "hyperliquid"; + const initialVenueB = VALID_VENUES.has(rawB) && rawB !== initialVenueA ? rawB : "gains"; + const initialWallet = /^0x[0-9a-fA-F]{40}$/.test(params.wallet ?? "") ? params.wallet! : ""; + const rawDays = parseInt(params.days ?? "90", 10); + const initialDays = isFinite(rawDays) ? Math.min(180, Math.max(7, rawDays)) : 90; + return (

@@ -28,7 +44,12 @@ export default function FeeComparePage() {

- +
); diff --git a/src/components/fee-compare-client.tsx b/src/components/fee-compare-client.tsx index a98e7b83..6e931a1c 100644 --- a/src/components/fee-compare-client.tsx +++ b/src/components/fee-compare-client.tsx @@ -75,6 +75,7 @@ type HlWalletData = { type GainsWalletData = { events: number; feesUsdc: number; + vaultFeesUsdc: number; fundingFeesUsdc: number; fundingEstimated: boolean; borrowingFeesUsdc: number; @@ -87,8 +88,10 @@ type GainsWalletData = { action: string; notional: number; tradingFee: number; + vaultFee: number; fundingFee: number; borrowingFee: number; + equivFee?: number; pnl_net: number; }>; }; @@ -732,6 +735,7 @@ function WalletSide({ })()} {venue.slug === "gains" && (() => { const gW = w as GainsWalletData; + const hasVault = (gW.vaultFeesUsdc ?? 0) > 0.5; const hasFunding = Math.abs(gW.fundingFeesUsdc) > 0.5; const hasBorrowing = gW.borrowingFeesUsdc > 0.5; return ( @@ -742,9 +746,20 @@ function WalletSide({
-

Trading fees

+

Taker fees

{fmtUsd(gW.feesUsdc)}

+ {hasVault && ( +
+

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

+

+ +{fmtUsd(gW.vaultFeesUsdc)} +

+
+ )} {hasFunding && (

@@ -770,10 +785,7 @@ function WalletSide({ )}

- Net cost{" "} - - {hasFunding || hasBorrowing ? "incl. carry" : "trading only"} - + Net cost all fees

{fmtUsd(gW.netCostUsdc)} @@ -1135,13 +1147,16 @@ function HlTradeTable({ function GainsTradeTable({ trades, venueName, + otherVenueName, }: { trades: GainsWalletData["recentTrades"]; venueName: string; + otherVenueName?: string; }) { const [showAll, setShowAll] = useState(false); const PREVIEW = 10; const rows = showAll ? trades : trades.slice(0, PREVIEW); + const hasEquiv = !!otherVenueName && trades.some((t) => t.equivFee !== undefined); if (trades.length === 0) return null; @@ -1159,7 +1174,9 @@ function GainsTradeTable({

-

{venueName} trade history

+

+ {hasEquiv ? `${venueName} vs ${otherVenueName} — per trade` : `${venueName} trade history`} +

{trades.length} actions

@@ -1171,15 +1188,18 @@ function GainsTradeTable({ Market Action Notional - Net cost + {venueName} fee + {hasEquiv && ( + {otherVenueName} fee + )} PnL {rows.map((t, i) => { - const carry = t.fundingFee + t.borrowingFee; - const netCost = t.tradingFee + carry; - const hasCarry = Math.abs(t.fundingFee) > 0.001 || t.borrowingFee > 0.001; + const hasVault = (t.vaultFee ?? 0) > 0.001; + const netCost = t.tradingFee + (t.vaultFee ?? 0) + t.fundingFee + t.borrowingFee; + const diff = hasEquiv && t.equivFee !== undefined ? t.equivFee - netCost : undefined; return ( @@ -1198,16 +1218,33 @@ function GainsTradeTable({ {fmtUsd(t.notional)}

{fmtUsd(netCost)}

- {hasCarry && ( -

- {fmtUsd(t.tradingFee)} fee - {t.borrowingFee > 0.001 ? ` +${fmtUsd(t.borrowingFee)} borrow` : ""} - {Math.abs(t.fundingFee) > 0.001 - ? ` ${t.fundingFee > 0 ? "+" : "−"}${fmtUsd(Math.abs(t.fundingFee))} fund` - : ""} -

- )} +

+ {fmtUsd(t.tradingFee)} taker + {hasVault ? ` +${fmtUsd(t.vaultFee)} vault` : ""} + {t.borrowingFee > 0.001 ? ` +${fmtUsd(t.borrowingFee)} borrow` : ""} + {Math.abs(t.fundingFee) > 0.001 + ? ` ${t.fundingFee > 0 ? "+" : "−"}${fmtUsd(Math.abs(t.fundingFee))} fund` + : ""} +

+ {hasEquiv && ( + + {t.equivFee !== undefined ? ( + <> +

netCost ? "text-red-400" : "text-ink-faint" + }`}>{fmtUsd(t.equivFee)}

+ {diff !== undefined && Math.abs(diff) > 0.01 && ( +

+ {diff < 0 ? `−${fmtUsd(Math.abs(diff))}` : `+${fmtUsd(diff)}`} +

+ )} + + ) : ( + n/a + )} + + )} {t.action.includes("Closed") ? ( t.pnl_net > 0.01 ? ( @@ -1426,10 +1463,10 @@ function Results({ result }: { result: FeeCompareResult }) { /> )} {gainsVenueA && gainsVenueA.recentTrades.length > 0 && ( - + )} {gainsVenueB && gainsVenueB.recentTrades.length > 0 && ( - + )} {gmxVenueA && gmxVenueA.recentTrades.length > 0 && ( @@ -1530,8 +1567,8 @@ export function FeeCompareClient({ const data = (await res.json()) as FeeCompareResult; setResult(data); if (trimmed) { - const newPath = `/fee-compare/${va}/${vb}/${trimmed}`; - window.history.pushState({}, "", newPath); + const qs = new URLSearchParams({ venueA: va, venueB: vb, wallet: trimmed, days: String(overrideDays ?? days) }); + window.history.pushState({}, "", `/fee-compare?${qs}`); } } catch { setError("Network error — check your connection."); diff --git a/src/data/provider-registry.ts b/src/data/provider-registry.ts index ef741132..169cffc4 100644 --- a/src/data/provider-registry.ts +++ b/src/data/provider-registry.ts @@ -2562,6 +2562,35 @@ export const PROVIDER_REGISTRY: Record = { "OKX Chain (OKTC) official public EVM RPC node (exchainrpc.okex.org). Provides standard Ethereum JSON-RPC for OKX Token Chain mainnet, no API key required.", twitter: "@OKXOfficial", }, + + // ─── Litecoin providers (bench 246) ────────────────────────── + litecoinspace: { + url: "https://litecoinspace.org", + description: + "LitecoinSpace is a Litecoin block explorer built on the Esplora API. Exposes the current tip height at /api/blocks/tip/height as a plain integer, no API key required.", + }, + + // ─── Interlay providers (bench 249) ────────────────────────── + "interlay-official": { + url: "https://api.interlay.io/parachain", + description: + "Interlay official public Substrate RPC node (api.interlay.io/parachain). Provides a keyless Substrate JSON-RPC interface for the Interlay parachain on Polkadot.", + twitter: "@InterlayHQ", + }, + + // ─── Acala providers (bench 248) ────────────────────────── + "acala-rpc0": { + url: "https://acala-rpc-0.aca-api.network", + description: + "Acala official public RPC node 0 (acala-rpc-0.aca-api.network). Keyless Substrate JSON-RPC interface for the Acala DeFi parachain on Polkadot.", + twitter: "@AcalaNetwork", + }, + "acala-rpc2": { + url: "https://acala-rpc-2.aca-api.network", + description: + "Acala official public RPC node 2 (acala-rpc-2.aca-api.network). Keyless Substrate JSON-RPC interface for the Acala DeFi parachain on Polkadot.", + twitter: "@AcalaNetwork", + }, }; /** diff --git a/src/lib/brand.ts b/src/lib/brand.ts index e0eb3a24..83d02a58 100644 --- a/src/lib/brand.ts +++ b/src/lib/brand.ts @@ -169,6 +169,13 @@ const BRANDS: Record = { tcinfra: { color: "#2C7DF7" }, // tcinfra tezos blue "oktc-official": { color: "#101010", dark: true }, // okx dark + // ─── Polkadot parachain RPC chains (benches 248-249) ─── + acala: { color: "#E40C5B" }, // acala red/pink (official brand) + interlay: { color: "#1A3BDB" }, // interlay blue (official brand) + + // ─── Cosmos SDK chains (bench 247) ─── + babylon: { color: "#F8811A" }, // babylon orange (official brand) + // ─── Bitcoin Cash chain + providers (bench 244) ─── "bitcoin-cash": { color: "#0AC18E" }, // bch official green bitcore: { color: "#1A1D21", dark: true }, // bitpay dark diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index 05dd2dce..0c20e4c4 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -323,6 +323,12 @@ const RAW: Record = { // ─── Buyback audit (bench 018) ─── sky: "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/logos/sky.svg", + // ─── New RPC chains (benches 246-249) ─── + babylon: "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/logos/babylon.svg", + acala: "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/logos/acala.svg", + interlay: "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/logos/interlay.svg", + litecoinspace: "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/logos/litecoinspace.svg", + // ─── Oracle deviation (bench 025) — additional brand logos ─── // (pairs alias to chain/asset logos in the ALIASES block below) chainlink: "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/logos/chainlink.svg", @@ -723,6 +729,13 @@ const ALIASES: Record = { "orai-official": "oraichain", "peaq-official": "peaq", + // Interlay official node alias → interlay chain logo + "interlay-official": "interlay", + + // Acala official node aliases → acala chain logo + "acala-rpc0": "acala", + "acala-rpc2": "acala", + // Non-EVM wave-3 (benches 222-231) — provider-official aliases to chain slug "ecadinfra": "tezos", "tzbeta": "tezos", diff --git a/src/lib/removed-benches.ts b/src/lib/removed-benches.ts index 5f87dcec..39d4ef30 100644 --- a/src/lib/removed-benches.ts +++ b/src/lib/removed-benches.ts @@ -157,5 +157,10 @@ export const REMOVED_PRODUCT_SLUGS = new Set([ // the provider in getProviders() output while the page 404s; 410 guard // + sitemap exclusion stop the smoke-gate rollback until Redis flushes. "goldrush", + // zerion and routescan appear in stale Redis results from retired benches + // (indexing-freshness for zerion, explorer-chain-coverage for routescan) + // but have no /products/ page. 410 guard prevents smoke-gate failures. + "zerion", + "routescan", ]); diff --git a/src/lib/sitemap-builder.ts b/src/lib/sitemap-builder.ts index 5ee65e88..d2d115c0 100644 --- a/src/lib/sitemap-builder.ts +++ b/src/lib/sitemap-builder.ts @@ -5,6 +5,7 @@ import { getAllReports, getAllReportCategories } from "@/lib/reports/loader"; import { COMPARE_PAIRS } from "@/data/compare-pairs"; import { REMOVED_BENCH_SLUGS } from "@/middleware"; import { REMOVED_PRODUCT_SLUGS } from "@/lib/removed-benches"; +import { isHlBuilderSlug } from "@/lib/hl-builder-stats"; import { PERP_PRODUCT_PILL_SLUGS } from "@/lib/perp-venue-context"; import { loadAllAlternatives } from "@/lib/alternatives"; import { loadAllAnswers } from "@/lib/answers"; @@ -270,15 +271,22 @@ async function buildFullSitemap(): Promise { // Provider routes. The worker pre-filters providerSlugs to exclude chain // slugs, HL builder slugs, perp venue slugs, and removed slugs. Apply the - // same O(1) set checks here as a safety net — no async getProvider() call - // needed (the 505-call fan-out was OOM-crashing the Vercel function). - const validatedSlugs = providerSlugs.filter((slug) => { - if (CHAIN_BY_SLUG.has(slug)) return false; - if (hlBuilderSlugSet.has(slug)) return false; - if (PERP_PRODUCT_PILL_SLUGS.has(slug) && slug !== "polymarket") return false; - if (REMOVED_PRODUCT_SLUGS.has(slug)) return false; - return true; - }); + // same checks here as a safety net. isHlBuilderSlug reads the spec (not + // Prom) so it's safe async — no OOM risk (unlike the old getProvider fan-out). + // It also catches dormant HL frontends missing from the Prom cohort that + // the worker couldn't filter without the spec provider list. + const validatedSlugs = ( + await Promise.all( + providerSlugs.map(async (slug) => { + if (CHAIN_BY_SLUG.has(slug)) return null; + if (hlBuilderSlugSet.has(slug)) return null; + if (await isHlBuilderSlug(slug)) return null; + if (PERP_PRODUCT_PILL_SLUGS.has(slug) && slug !== "polymarket") return null; + if (REMOVED_PRODUCT_SLUGS.has(slug)) return null; + return slug; + }), + ) + ).filter((s): s is string => s !== null); const providerRoutes: MetadataRoute.Sitemap = validatedSlugs.map((slug) => ({ url: `${SITE.url}/products/${slug}`, @@ -357,11 +365,14 @@ async function buildFullSitemap(): Promise { })); // Category hub pages. Filter to categories that have live benches. - const liveCategoryLabels = new Set(blobBenches.map((b) => b.category)); + // Exclude REMOVED_BENCH_SLUGS so benches with stale Redis data (410 on + // prod) don't keep their category hub alive in the sitemap. + const activeBlobBenches = blobBenches.filter((b) => !REMOVED_BENCH_SLUGS.has(b.slug)); + const liveCategoryLabels = new Set(activeBlobBenches.map((b) => b.category)); const categoryRoutes: MetadataRoute.Sitemap = CATEGORIES .filter((c) => liveCategoryLabels.has(c.label)) .map((c) => { - const catBenches = blobBenches.filter((b) => b.category === c.label); + const catBenches = activeBlobBenches.filter((b) => b.category === c.label); const last = catBenches.reduce((acc, b) => { if (!b.lastRunAt) return acc; const t = new Date(b.lastRunAt); diff --git a/src/lib/spec.ts b/src/lib/spec.ts index 34604a14..d873e51a 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -328,7 +328,7 @@ const loadBenchmarkUnfilteredCached = unstable_cache( // v55: add ws-head-latency-robinhood (244) + keyed-rpc-robinhood (243). Bench SET grew. // v56: drop 6 keyed-rpc benches (arbitrum/base/bnb/eth/polygon/solana), US-only robinhood. // v57: drop rpc-keyed-latency bench. - ["bench-unfiltered-v58", process.env.VERCEL_ENV === "production" ? "prod" : "all"], + ["bench-unfiltered-v62", process.env.VERCEL_ENV === "production" ? "prod" : "all"], { revalidate: 300, tags: ["benchmarks"] }, ); @@ -639,7 +639,7 @@ const loadBenchmarkFiltered = unstable_cache( // v21: bumped with bench-unfiltered-v46 (network-coverage split). // v23: lockstep with bench-unfiltered-v56 (keyed-rpc cleanup). // v24: lockstep with bench-unfiltered-v57 (drop rpc-keyed-latency). - ["bench-filters-v25", process.env.VERCEL_ENV === "production" ? "prod" : "all"], + ["bench-filters-v29", process.env.VERCEL_ENV === "production" ? "prod" : "all"], { revalidate: 300, tags: ["benchmarks"] } ); diff --git a/worker/index.ts b/worker/index.ts index 76330e92..ca90f1f7 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -444,7 +444,16 @@ async function sweep(iteration: number): Promise { ); if (process.env.AGGREGATE_OUTPUT_PATH && publishedBenches.length > 0) { - const slimResult = await publishSitemapSlim(publishedBenches, hlBuilders); + // Augment hlBuilders (Prom cohort, active builders only) with every + // slug from the hyperliquid-frontends spec so builders without recent + // Prom data aren't emitted as /products/ (which 308-redirects + // to /hyperliquid/ and fails the smoke gate with non-200). + const hlFrontendSpec = specs.find((s) => s.slug === "hyperliquid-frontends"); + const hlFrontendSpecSlugs = (hlFrontendSpec?.providers ?? []).map( + (p: { slug: string }) => p.slug, + ); + const allHlSlugs = [...new Set([...hlBuilders, ...hlFrontendSpecSlugs])]; + const slimResult = await publishSitemapSlim(publishedBenches, allHlSlugs); if (slimResult.ok) { console.log(`[worker] sitemap slim published (${slimResult.bytes} bytes)`); } else { diff --git a/worker/publish-aggregate.ts b/worker/publish-aggregate.ts index 79ad94cf..c84080b1 100644 --- a/worker/publish-aggregate.ts +++ b/worker/publish-aggregate.ts @@ -32,7 +32,7 @@ import type { Benchmark } from "@/types/benchmark"; import type { Spec } from "@/lib/spec-schema"; import { CHAIN_BY_SLUG } from "@/lib/chains"; import { PERP_PRODUCT_PILL_SLUGS } from "@/lib/perp-venue-context"; -import { REMOVED_PRODUCT_SLUGS } from "@/lib/removed-benches"; +import { REMOVED_BENCH_SLUGS, REMOVED_PRODUCT_SLUGS } from "@/lib/removed-benches"; import { draftPlaceholderForSpec, filterSig, @@ -237,6 +237,10 @@ export async function publishSitemapSlim( const slimBenches = benches .filter((b) => { if (b.status !== "live" || b.editorialStatus !== "live") return false; + // Bench slugs in REMOVED_BENCH_SLUGS have stale Redis data but return + // 410 on prod via middleware. Emitting them in the sitemap would cause + // the smoke gate to see 410s (→ non-200) and fail the deploy. + if (REMOVED_BENCH_SLUGS.has(b.slug)) return false; // Mirror bench page noindex gate: RPC benches with <3 providers are // thin-content and render noindex. Emitting them fails the smoke gate. if (b.category === "RPCs" && (b.results?.length ?? 0) < 3) return false;