From 8c2315c8111465df3d6229d33a46e7b7e09eb1cb Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:42:48 +0200 Subject: [PATCH 01/39] fix: remove dead providers (BlastAPI shutdown, Ankr key-gated, dRPC/1RPC not supported) --- benchmarks/chiliz-rpc.yml | 23 ------- benchmarks/etherlink-rpc.yml | 46 ------------- benchmarks/fantom-rpc.yml | 46 ------------- benchmarks/haqq-rpc.yml | 23 ------- benchmarks/kusama-rpc.yml | 69 ------------------- benchmarks/oraichain-rpc.yml | 23 ------- benchmarks/peaq-rpc.yml | 23 ------- benchmarks/songbird-rpc.yml | 23 ------- benchmarks/telos-rpc.yml | 23 ------- benchmarks/warden-rpc.yml | 23 ------- benchmarks/wemix-rpc.yml | 46 ------------- benchmarks/zetachain-rpc.yml | 23 ------- .../rpc-capabilities/cmd/script/config.go | 46 ++++--------- 13 files changed, 14 insertions(+), 423 deletions(-) diff --git a/benchmarks/chiliz-rpc.yml b/benchmarks/chiliz-rpc.yml index bf754b6a..05116b2f 100644 --- a/benchmarks/chiliz-rpc.yml +++ b/benchmarks/chiliz-rpc.yml @@ -130,26 +130,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="chiliz-official", chain="chiliz", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="chiliz-official", chain="chiliz", region="sgp"}[1h]) - - - slug: drpc - name: dRPC - tag: dRPC public gateway for Chiliz Chain - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to dRPC's no-key Chiliz endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="chiliz"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="chiliz"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="chiliz"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="chiliz"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="chiliz"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="chiliz"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="chiliz"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="chiliz"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="chiliz", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="chiliz", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="chiliz", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="chiliz", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="chiliz", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="chiliz", region="sgp"}[1h]) diff --git a/benchmarks/etherlink-rpc.yml b/benchmarks/etherlink-rpc.yml index 0edd3e16..a8e0bdb3 100644 --- a/benchmarks/etherlink-rpc.yml +++ b/benchmarks/etherlink-rpc.yml @@ -107,49 +107,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="etherlink", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="etherlink", region="sgp"}[1h]) - - - slug: ankr - name: Ankr - tag: Ankr decentralized node provider for Etherlink - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Ankr's no-key Etherlink endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="etherlink"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="ankr", chain="etherlink"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="ankr", chain="etherlink"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="ankr", chain="etherlink"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="ankr", chain="etherlink"}) / sum(ocb:rpc_call:rate_24h{provider="ankr", chain="etherlink"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="ankr", chain="etherlink"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="etherlink"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="etherlink", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="etherlink", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="etherlink", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="etherlink", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="etherlink", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="etherlink", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for Etherlink mainnet - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key Etherlink endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="etherlink"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="etherlink"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="etherlink"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="etherlink"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="etherlink"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="etherlink"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="etherlink"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="etherlink"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="etherlink", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="etherlink", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="etherlink", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="etherlink", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="etherlink", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="etherlink", region="sgp"}[1h]) diff --git a/benchmarks/fantom-rpc.yml b/benchmarks/fantom-rpc.yml index 5c3668e7..c3c93b74 100644 --- a/benchmarks/fantom-rpc.yml +++ b/benchmarks/fantom-rpc.yml @@ -111,29 +111,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="fantom", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="fantom", region="sgp"}[1h]) - - slug: ankr - name: Ankr - tag: Ankr decentralized node provider for Fantom - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Ankr's no-key Fantom endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="fantom"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="ankr", chain="fantom"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="ankr", chain="fantom"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="ankr", chain="fantom"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="ankr", chain="fantom"}) / sum(ocb:rpc_call:rate_24h{provider="ankr", chain="fantom"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="ankr", chain="fantom"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="fantom"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="fantom", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="fantom", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="fantom", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="fantom", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="fantom", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="fantom", region="sgp"}[1h]) - - slug: thirdweb name: Thirdweb tag: Thirdweb chain-ID RPC gateway for Fantom @@ -156,26 +133,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="fantom", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="fantom", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for Fantom mainnet - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key Fantom endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="fantom"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="fantom"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="fantom"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="fantom"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="fantom"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="fantom"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="fantom"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="fantom"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="fantom", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="fantom", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="fantom", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="fantom", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="fantom", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="fantom", region="sgp"}[1h]) diff --git a/benchmarks/haqq-rpc.yml b/benchmarks/haqq-rpc.yml index bd4ba78b..d397c893 100644 --- a/benchmarks/haqq-rpc.yml +++ b/benchmarks/haqq-rpc.yml @@ -130,26 +130,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="haqq-official", chain="haqq", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="haqq-official", chain="haqq", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for HAQQ Network - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key HAQQ endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="haqq"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="haqq"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="haqq"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="haqq"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="haqq"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="haqq"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="haqq"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="haqq"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="haqq", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="haqq", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="haqq", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="haqq", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="haqq", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="haqq", region="sgp"}[1h]) diff --git a/benchmarks/kusama-rpc.yml b/benchmarks/kusama-rpc.yml index a69eb843..3f6e96d9 100644 --- a/benchmarks/kusama-rpc.yml +++ b/benchmarks/kusama-rpc.yml @@ -133,72 +133,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="kusama", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="kusama", region="sgp"}[1h]) - - - slug: ibp - name: IBP Network - tag: Kusama governance-funded infrastructure provider - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to IBP Network's no-key Kusama endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ibp", chain="kusama"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="ibp", chain="kusama"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="ibp", chain="kusama"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="ibp", chain="kusama"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="ibp", chain="kusama"}) / sum(ocb:rpc_call:rate_24h{provider="ibp", chain="kusama"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="ibp", chain="kusama"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="ibp", chain="kusama"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ibp", chain="kusama", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ibp", chain="kusama", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ibp", chain="kusama", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ibp", chain="kusama", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ibp", chain="kusama", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ibp", chain="kusama", region="sgp"}[1h]) - - - slug: dwellir - name: Dwellir - tag: Dwellir Substrate infrastructure provider - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to Dwellir's no-key Kusama endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="kusama"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="dwellir", chain="kusama"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="dwellir", chain="kusama"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="dwellir", chain="kusama"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="dwellir", chain="kusama"}) / sum(ocb:rpc_call:rate_24h{provider="dwellir", chain="kusama"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="dwellir", chain="kusama"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="kusama"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="kusama", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="kusama", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="kusama", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="kusama", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="kusama", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="kusama", region="sgp"}[1h]) - - - slug: "1rpc" - name: 1RPC - tag: 1RPC privacy-preserving relay for Kusama - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to 1RPC's no-key Kusama endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="kusama"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="1rpc", chain="kusama"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="1rpc", chain="kusama"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="1rpc", chain="kusama"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="1rpc", chain="kusama"}) / sum(ocb:rpc_call:rate_24h{provider="1rpc", chain="kusama"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="1rpc", chain="kusama"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="kusama"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="kusama", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="kusama", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="kusama", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="kusama", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="kusama", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="kusama", region="sgp"}[1h]) diff --git a/benchmarks/oraichain-rpc.yml b/benchmarks/oraichain-rpc.yml index 36fe5517..dd578387 100644 --- a/benchmarks/oraichain-rpc.yml +++ b/benchmarks/oraichain-rpc.yml @@ -107,26 +107,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="orai-official", chain="oraichain", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="orai-official", chain="oraichain", region="sgp"}[1h]) - - - slug: lavenderfive - name: LavenderFive - tag: LavenderFive validator and RPC provider for Cosmos chains - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single Tendermint status POST sent every 60s from 3 regions to LavenderFive's no-key Oraichain endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lavenderfive", chain="oraichain"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="lavenderfive", chain="oraichain"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="lavenderfive", chain="oraichain"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="lavenderfive", chain="oraichain"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="lavenderfive", chain="oraichain"}) / sum(ocb:rpc_call:rate_24h{provider="lavenderfive", chain="oraichain"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="lavenderfive", chain="oraichain"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="lavenderfive", chain="oraichain"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lavenderfive", chain="oraichain", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="lavenderfive", chain="oraichain", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lavenderfive", chain="oraichain", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="lavenderfive", chain="oraichain", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lavenderfive", chain="oraichain", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="lavenderfive", chain="oraichain", region="sgp"}[1h]) diff --git a/benchmarks/peaq-rpc.yml b/benchmarks/peaq-rpc.yml index 7a7c34f4..792d0ac2 100644 --- a/benchmarks/peaq-rpc.yml +++ b/benchmarks/peaq-rpc.yml @@ -85,29 +85,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="peaq", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="peaq", region="sgp"}[1h]) - - slug: peaq-official - name: Peaq Foundation - tag: Peaq Foundation official public RPC endpoint - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to Peaq Foundation's no-key endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="peaq-official", chain="peaq"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="peaq-official", chain="peaq"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="peaq-official", chain="peaq"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="peaq-official", chain="peaq"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="peaq-official", chain="peaq"}) / sum(ocb:rpc_call:rate_24h{provider="peaq-official", chain="peaq"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="peaq-official", chain="peaq"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="peaq-official", chain="peaq"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="peaq-official", chain="peaq", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="peaq-official", chain="peaq", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="peaq-official", chain="peaq", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="peaq-official", chain="peaq", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="peaq-official", chain="peaq", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="peaq-official", chain="peaq", region="sgp"}[1h]) - - slug: onfinality name: OnFinality tag: OnFinality Substrate public gateway for Peaq diff --git a/benchmarks/songbird-rpc.yml b/benchmarks/songbird-rpc.yml index db727d94..d8207bac 100644 --- a/benchmarks/songbird-rpc.yml +++ b/benchmarks/songbird-rpc.yml @@ -108,29 +108,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="songbird", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="songbird", region="sgp"}[1h]) - - slug: blastapi - name: Blast API - tag: Blast API public gateway for Songbird - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key Songbird endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="songbird"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="songbird"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="songbird"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="songbird"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="songbird"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="songbird"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="songbird"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="songbird"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="songbird", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="songbird", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="songbird", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="songbird", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="songbird", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="songbird", region="sgp"}[1h]) - - slug: ftso-au name: FTSO AU tag: ftso.com.au community Songbird RPC node diff --git a/benchmarks/telos-rpc.yml b/benchmarks/telos-rpc.yml index 53136be7..00c3d939 100644 --- a/benchmarks/telos-rpc.yml +++ b/benchmarks/telos-rpc.yml @@ -107,26 +107,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="telos", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="telos", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for Telos EVM - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key Telos endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="telos"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="telos"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="telos"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="telos"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="telos"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="telos"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="telos"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="telos"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="telos", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="telos", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="telos", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="telos", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="telos", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="telos", region="sgp"}[1h]) diff --git a/benchmarks/warden-rpc.yml b/benchmarks/warden-rpc.yml index 0bb99648..4aa2f76c 100644 --- a/benchmarks/warden-rpc.yml +++ b/benchmarks/warden-rpc.yml @@ -85,29 +85,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="warden", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="warden", region="sgp"}[1h]) - - slug: drpc - name: dRPC - tag: dRPC public gateway for Warden Protocol - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to dRPC's no-key Warden endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="warden"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="warden"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="warden"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="warden"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="warden"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="warden"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="warden"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="warden"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="warden", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="warden", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="warden", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="warden", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="warden", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="warden", region="sgp"}[1h]) - - slug: warden-official name: Warden Foundation tag: Warden Foundation official public EVM RPC diff --git a/benchmarks/wemix-rpc.yml b/benchmarks/wemix-rpc.yml index 9b3772d5..62f9b5ac 100644 --- a/benchmarks/wemix-rpc.yml +++ b/benchmarks/wemix-rpc.yml @@ -107,49 +107,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="wemix", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="wemix", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for WEMIX 3.0 - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key WEMIX endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="wemix"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="wemix"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="wemix"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="wemix"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="wemix"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="wemix"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="wemix"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="wemix"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="wemix", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="wemix", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="wemix", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="wemix", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="wemix", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="wemix", region="sgp"}[1h]) - - - slug: "1rpc" - name: 1RPC - tag: 1RPC privacy-preserving relay for WEMIX - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to 1RPC's no-key WEMIX endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="wemix"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="1rpc", chain="wemix"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="1rpc", chain="wemix"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="1rpc", chain="wemix"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="1rpc", chain="wemix"}) / sum(ocb:rpc_call:rate_24h{provider="1rpc", chain="wemix"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="1rpc", chain="wemix"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="wemix"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="wemix", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="wemix", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="wemix", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="wemix", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="wemix", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="wemix", region="sgp"}[1h]) diff --git a/benchmarks/zetachain-rpc.yml b/benchmarks/zetachain-rpc.yml index 2b9a1bba..c53c320d 100644 --- a/benchmarks/zetachain-rpc.yml +++ b/benchmarks/zetachain-rpc.yml @@ -153,26 +153,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="allthatnode", chain="zetachain", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="allthatnode", chain="zetachain", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for ZetaChain mainnet - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key ZetaChain endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="zetachain"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="zetachain"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="zetachain"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="zetachain"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="zetachain"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="zetachain"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="zetachain"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="zetachain"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="zetachain", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="zetachain", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="zetachain", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="zetachain", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="zetachain", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="zetachain", region="sgp"}[1h]) diff --git a/harnesses/rpc-capabilities/cmd/script/config.go b/harnesses/rpc-capabilities/cmd/script/config.go index ab02e508..325b2daf 100644 --- a/harnesses/rpc-capabilities/cmd/script/config.go +++ b/harnesses/rpc-capabilities/cmd/script/config.go @@ -1339,19 +1339,17 @@ func chains() []Chain { {Slug: "stavr", Name: "STAVR", URL: envDefault("RPC_URL_COMDEX_STAVR", "https://comdex.rpc.m.stavr.tech:443")}, }, }, - // Fantom — EVM (chain 250). 5 keyless providers. + // Fantom — EVM (chain 250). 3 keyless providers. { Slug: "fantom", Name: "Fantom", Providers: []Provider{ {Slug: "fantom-official", Name: "Fantom Foundation", URL: envDefault("RPC_URL_FANTOM_OFFICIAL", "https://rpcapi.fantom.network")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_FANTOM_DRPC", "https://fantom.drpc.org")}, - {Slug: "ankr", Name: "Ankr", URL: envDefault("RPC_URL_FANTOM_ANKR", "https://rpc.ankr.com/fantom")}, {Slug: "thirdweb", Name: "Thirdweb", URL: envDefault("RPC_URL_FANTOM_THIRDWEB", "https://250.rpc.thirdweb.com")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_FANTOM_BLASTAPI", "https://fantom-mainnet.public.blastapi.io")}, }, }, - // Kusama — Polkadot canary relay chain. 6 keyless providers. + // Kusama — Polkadot canary relay chain. 3 keyless providers. { Slug: "kusama", Name: "Kusama", @@ -1360,9 +1358,6 @@ func chains() []Chain { {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_KUSAMA_PUBLICNODE", "https://kusama-rpc.publicnode.com")}, {Slug: "onfinality", Name: "OnFinality", URL: envDefault("RPC_URL_KUSAMA_ONFINALITY", "https://kusama.api.onfinality.io/public")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_KUSAMA_DRPC", "https://kusama.drpc.org")}, - {Slug: "ibp", Name: "IBP Network", URL: envDefault("RPC_URL_KUSAMA_IBP", "https://rpc.ibp.network/kusama")}, - {Slug: "dwellir", Name: "Dwellir", URL: envDefault("RPC_URL_KUSAMA_DWELLIR", "https://kusama-rpc.dwellir.com")}, - {Slug: "1rpc", Name: "1RPC", URL: envDefault("RPC_URL_KUSAMA_1RPC", "https://1rpc.io/ksm")}, }, }, // Hydration — Polkadot DeFi parachain (HydraDX). 5 keyless providers. @@ -1378,7 +1373,7 @@ func chains() []Chain { {Slug: "parachains-network", Name: "Parachains.network", URL: envDefault("RPC_URL_HYDRATION_PARACHAINS", "https://rpc.parachains.network/hydradx")}, }, }, - // ZetaChain — EVM omnichain L1 (chain 7000). 5 keyless providers. + // ZetaChain — EVM omnichain L1 (chain 7000). 4 keyless providers. { Slug: "zetachain", Name: "ZetaChain", @@ -1387,10 +1382,9 @@ func chains() []Chain { {Slug: "blockpi", Name: "BlockPi", URL: envDefault("RPC_URL_ZETACHAIN_BLOCKPI", "https://zetachain.blockpi.network/v1/rpc/public")}, {Slug: "thirdweb", Name: "Thirdweb", URL: envDefault("RPC_URL_ZETACHAIN_THIRDWEB", "https://7000.rpc.thirdweb.com")}, {Slug: "allthatnode", Name: "AllThatNode", URL: envDefault("RPC_URL_ZETACHAIN_ATN", "https://zetachain-mainnet.rpc.public.allthatnode.com")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_ZETACHAIN_BLASTAPI", "https://zetachain-mainnet.public.blastapi.io")}, }, }, - // HAQQ — EVM Islamic finance L1 (chain 11235). 4 keyless providers. + // HAQQ — EVM Islamic finance L1 (chain 11235). 3 keyless providers. { Slug: "haqq", Name: "HAQQ", @@ -1398,21 +1392,18 @@ func chains() []Chain { {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_HAQQ_PUBLICNODE", "https://haqq-evm-rpc.publicnode.com")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_HAQQ_DRPC", "https://haqq.drpc.org")}, {Slug: "haqq-official", Name: "HAQQ Foundation", URL: envDefault("RPC_URL_HAQQ_OFFICIAL", "https://rpc.eth.haqq.network")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_HAQQ_BLASTAPI", "https://haqq-network.public.blastapi.io")}, }, }, - // Etherlink — Tezos EVM L2 (chain 42793). 4 keyless providers. + // Etherlink — Tezos EVM L2 (chain 42793). 2 keyless providers. { Slug: "etherlink", Name: "Etherlink", Providers: []Provider{ {Slug: "etherlink-official", Name: "Etherlink Foundation", URL: envDefault("RPC_URL_ETHERLINK_OFFICIAL", "https://node.mainnet.etherlink.com")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_ETHERLINK_DRPC", "https://etherlink.drpc.org")}, - {Slug: "ankr", Name: "Ankr", URL: envDefault("RPC_URL_ETHERLINK_ANKR", "https://rpc.ankr.com/etherlink")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_ETHERLINK_BLASTAPI", "https://etherlink-mainnet.public.blastapi.io")}, }, }, - // Chiliz — Sports fan token EVM (chain 88888). 4 keyless providers. + // Chiliz — Sports fan token EVM (chain 88888). 3 keyless providers. { Slug: "chiliz", Name: "Chiliz", @@ -1420,39 +1411,34 @@ func chains() []Chain { {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_CHILIZ_PUBLICNODE", "https://chiliz-rpc.publicnode.com")}, {Slug: "ankr", Name: "Ankr", URL: envDefault("RPC_URL_CHILIZ_ANKR", "https://rpc.ankr.com/chiliz")}, {Slug: "chiliz-official", Name: "Chiliz Foundation", URL: envDefault("RPC_URL_CHILIZ_OFFICIAL", "https://rpc.chiliz.com")}, - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_CHILIZ_DRPC", "https://chiliz.drpc.org")}, }, }, - // WEMIX — Korean web3 gaming EVM (chain 1111). 4 keyless providers. + // WEMIX — Korean web3 gaming EVM (chain 1111). 2 keyless providers. { Slug: "wemix", Name: "WEMIX", Providers: []Provider{ - {Slug: "wemix-official", Name: "WEMIX Foundation", URL: envDefault("RPC_URL_WEMIX_OFFICIAL", "https://api.wemix.com")}, + {Slug: "wemix-official", Name: "WeMade", URL: envDefault("RPC_URL_WEMIX_OFFICIAL", "https://api.wemix.com")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_WEMIX_DRPC", "https://wemix.drpc.org")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_WEMIX_BLASTAPI", "https://wemix-mainnet.public.blastapi.io")}, - {Slug: "1rpc", Name: "1RPC", URL: envDefault("RPC_URL_WEMIX_1RPC", "https://1rpc.io/wemix")}, }, }, - // Songbird — Flare canary network (chain 19). 4 keyless providers. + // Songbird — Flare canary network (chain 19). 3 keyless providers. { Slug: "songbird", Name: "Songbird", Providers: []Provider{ {Slug: "flare-official", Name: "Flare Foundation", URL: envDefault("RPC_URL_SONGBIRD_FLARE", "https://songbird-api.flare.network/ext/C/rpc")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_SONGBIRD_DRPC", "https://songbird.drpc.org")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_SONGBIRD_BLASTAPI", "https://songbird-mainnet.public.blastapi.io")}, {Slug: "ftso-au", Name: "FTSO AU", URL: envDefault("RPC_URL_SONGBIRD_FTSOAU", "https://songbird.rpc.ftso.au")}, }, }, - // Cronos zkEVM — zkSync-stack Cronos L2 (chain 388). 3 keyless providers. + // Cronos zkEVM — zkSync-stack Cronos L2 (chain 388). 2 keyless providers. { Slug: "cronos-zkevm", Name: "Cronos zkEVM", Providers: []Provider{ {Slug: "cronos-zkevm-official", Name: "Crypto.com Foundation", URL: envDefault("RPC_URL_CRONOS_ZKEVM_OFFICIAL", "https://mainnet.zkevm.cronos.org")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_CRONOS_ZKEVM_DRPC", "https://cronos-zkevm.drpc.org")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_CRONOS_ZKEVM_BLASTAPI", "https://cronos-zkevm-mainnet.public.blastapi.io")}, }, }, // Ethereum Classic — Original Ethereum chain (chain 61). 3 keyless providers. @@ -1465,14 +1451,13 @@ func chains() []Chain { {Slug: "etcmc", Name: "ETCMC", URL: envDefault("RPC_URL_ETC_ETCMC", "https://etcmc.rpc.nz")}, }, }, - // Telos EVM — High-performance EVM (chain 40). 3 keyless providers. + // Telos EVM — High-performance EVM (chain 40). 2 keyless providers. { Slug: "telos", Name: "Telos", Providers: []Provider{ {Slug: "telos-official", Name: "Telos Foundation", URL: envDefault("RPC_URL_TELOS_OFFICIAL", "https://mainnet.telos.net/evm")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_TELOS_DRPC", "https://telos.drpc.org")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_TELOS_BLASTAPI", "https://telos-mainnet.public.blastapi.io")}, }, }, // PulseChain — Full-state Ethereum fork (chain 369). 3 keyless providers. @@ -1485,17 +1470,16 @@ func chains() []Chain { {Slug: "g4mm4", Name: "G4MM4", URL: envDefault("RPC_URL_PULSECHAIN_G4MM4", "https://rpc-pulsechain.g4mm4.io")}, }, }, - // Warden Protocol — Intent-based EVM L1. 3 keyless providers. + // Warden Protocol — Intent-based EVM L1. 2 keyless providers. { Slug: "warden", Name: "Warden Protocol", Providers: []Provider{ {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_WARDEN_PUBLICNODE", "https://warden-evm-rpc.publicnode.com")}, - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_WARDEN_DRPC", "https://warden.drpc.org")}, {Slug: "warden-official", Name: "Warden Foundation", URL: envDefault("RPC_URL_WARDEN_OFFICIAL", "https://evm.wardenprotocol.org")}, }, }, - // Oraichain — AI-focused Cosmos SDK chain. 3 keyless providers. + // Oraichain — AI-focused Cosmos SDK chain. 2 keyless providers. { Slug: "oraichain", Name: "Oraichain", @@ -1503,17 +1487,15 @@ func chains() []Chain { Providers: []Provider{ {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_ORAICHAIN_PUBLICNODE", "https://oraichain-rpc.publicnode.com")}, {Slug: "orai-official", Name: "Orai Foundation", URL: envDefault("RPC_URL_ORAICHAIN_OFFICIAL", "https://rpc.orai.io")}, - {Slug: "lavenderfive", Name: "LavenderFive", URL: envDefault("RPC_URL_ORAICHAIN_LAVENDERFIVE", "https://oraichain-rpc.lavenderfive.com")}, }, }, - // Peaq Network — DePIN Polkadot parachain. 3 keyless providers. + // Peaq Network — DePIN Polkadot parachain. 2 keyless providers. { Slug: "peaq", Name: "Peaq Network", Kind: "polkadot", Providers: []Provider{ {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_PEAQ_PUBLICNODE", "https://peaq-rpc.publicnode.com")}, - {Slug: "peaq-official", Name: "Peaq Foundation", URL: envDefault("RPC_URL_PEAQ_OFFICIAL", "https://rpc.peaq.network")}, {Slug: "onfinality", Name: "OnFinality", URL: envDefault("RPC_URL_PEAQ_ONFINALITY", "https://peaq.api.onfinality.io/public")}, }, }, From 4497d88fdebf39cd627bed3bfdf7e88350498f51 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:44:08 +0200 Subject: [PATCH 02/39] fix: remove dead providers (BlastAPI shutdown, Ankr key-gated, dRPC/1RPC not supported) (#1789) --- benchmarks/chiliz-rpc.yml | 23 ------- benchmarks/etherlink-rpc.yml | 46 ------------- benchmarks/fantom-rpc.yml | 46 ------------- benchmarks/haqq-rpc.yml | 23 ------- benchmarks/kusama-rpc.yml | 69 ------------------- benchmarks/oraichain-rpc.yml | 23 ------- benchmarks/peaq-rpc.yml | 23 ------- benchmarks/songbird-rpc.yml | 23 ------- benchmarks/telos-rpc.yml | 23 ------- benchmarks/warden-rpc.yml | 23 ------- benchmarks/wemix-rpc.yml | 46 ------------- benchmarks/zetachain-rpc.yml | 23 ------- .../rpc-capabilities/cmd/script/config.go | 46 ++++--------- 13 files changed, 14 insertions(+), 423 deletions(-) diff --git a/benchmarks/chiliz-rpc.yml b/benchmarks/chiliz-rpc.yml index bf754b6a..05116b2f 100644 --- a/benchmarks/chiliz-rpc.yml +++ b/benchmarks/chiliz-rpc.yml @@ -130,26 +130,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="chiliz-official", chain="chiliz", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="chiliz-official", chain="chiliz", region="sgp"}[1h]) - - - slug: drpc - name: dRPC - tag: dRPC public gateway for Chiliz Chain - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to dRPC's no-key Chiliz endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="chiliz"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="chiliz"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="chiliz"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="chiliz"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="chiliz"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="chiliz"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="chiliz"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="chiliz"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="chiliz", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="chiliz", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="chiliz", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="chiliz", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="chiliz", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="chiliz", region="sgp"}[1h]) diff --git a/benchmarks/etherlink-rpc.yml b/benchmarks/etherlink-rpc.yml index 0edd3e16..a8e0bdb3 100644 --- a/benchmarks/etherlink-rpc.yml +++ b/benchmarks/etherlink-rpc.yml @@ -107,49 +107,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="etherlink", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="etherlink", region="sgp"}[1h]) - - - slug: ankr - name: Ankr - tag: Ankr decentralized node provider for Etherlink - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Ankr's no-key Etherlink endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="etherlink"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="ankr", chain="etherlink"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="ankr", chain="etherlink"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="ankr", chain="etherlink"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="ankr", chain="etherlink"}) / sum(ocb:rpc_call:rate_24h{provider="ankr", chain="etherlink"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="ankr", chain="etherlink"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="etherlink"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="etherlink", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="etherlink", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="etherlink", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="etherlink", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="etherlink", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="etherlink", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for Etherlink mainnet - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key Etherlink endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="etherlink"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="etherlink"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="etherlink"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="etherlink"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="etherlink"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="etherlink"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="etherlink"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="etherlink"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="etherlink", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="etherlink", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="etherlink", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="etherlink", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="etherlink", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="etherlink", region="sgp"}[1h]) diff --git a/benchmarks/fantom-rpc.yml b/benchmarks/fantom-rpc.yml index 5c3668e7..c3c93b74 100644 --- a/benchmarks/fantom-rpc.yml +++ b/benchmarks/fantom-rpc.yml @@ -111,29 +111,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="fantom", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="fantom", region="sgp"}[1h]) - - slug: ankr - name: Ankr - tag: Ankr decentralized node provider for Fantom - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Ankr's no-key Fantom endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="fantom"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="ankr", chain="fantom"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="ankr", chain="fantom"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="ankr", chain="fantom"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="ankr", chain="fantom"}) / sum(ocb:rpc_call:rate_24h{provider="ankr", chain="fantom"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="ankr", chain="fantom"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="fantom"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="fantom", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="fantom", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="fantom", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="fantom", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="fantom", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="fantom", region="sgp"}[1h]) - - slug: thirdweb name: Thirdweb tag: Thirdweb chain-ID RPC gateway for Fantom @@ -156,26 +133,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="fantom", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="fantom", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for Fantom mainnet - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key Fantom endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="fantom"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="fantom"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="fantom"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="fantom"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="fantom"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="fantom"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="fantom"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="fantom"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="fantom", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="fantom", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="fantom", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="fantom", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="fantom", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="fantom", region="sgp"}[1h]) diff --git a/benchmarks/haqq-rpc.yml b/benchmarks/haqq-rpc.yml index bd4ba78b..d397c893 100644 --- a/benchmarks/haqq-rpc.yml +++ b/benchmarks/haqq-rpc.yml @@ -130,26 +130,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="haqq-official", chain="haqq", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="haqq-official", chain="haqq", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for HAQQ Network - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key HAQQ endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="haqq"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="haqq"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="haqq"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="haqq"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="haqq"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="haqq"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="haqq"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="haqq"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="haqq", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="haqq", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="haqq", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="haqq", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="haqq", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="haqq", region="sgp"}[1h]) diff --git a/benchmarks/kusama-rpc.yml b/benchmarks/kusama-rpc.yml index a69eb843..3f6e96d9 100644 --- a/benchmarks/kusama-rpc.yml +++ b/benchmarks/kusama-rpc.yml @@ -133,72 +133,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="kusama", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="kusama", region="sgp"}[1h]) - - - slug: ibp - name: IBP Network - tag: Kusama governance-funded infrastructure provider - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to IBP Network's no-key Kusama endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ibp", chain="kusama"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="ibp", chain="kusama"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="ibp", chain="kusama"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="ibp", chain="kusama"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="ibp", chain="kusama"}) / sum(ocb:rpc_call:rate_24h{provider="ibp", chain="kusama"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="ibp", chain="kusama"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="ibp", chain="kusama"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ibp", chain="kusama", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ibp", chain="kusama", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ibp", chain="kusama", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ibp", chain="kusama", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ibp", chain="kusama", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ibp", chain="kusama", region="sgp"}[1h]) - - - slug: dwellir - name: Dwellir - tag: Dwellir Substrate infrastructure provider - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to Dwellir's no-key Kusama endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="kusama"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="dwellir", chain="kusama"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="dwellir", chain="kusama"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="dwellir", chain="kusama"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="dwellir", chain="kusama"}) / sum(ocb:rpc_call:rate_24h{provider="dwellir", chain="kusama"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="dwellir", chain="kusama"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="kusama"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="kusama", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="kusama", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="kusama", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="kusama", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="kusama", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="kusama", region="sgp"}[1h]) - - - slug: "1rpc" - name: 1RPC - tag: 1RPC privacy-preserving relay for Kusama - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to 1RPC's no-key Kusama endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="kusama"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="1rpc", chain="kusama"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="1rpc", chain="kusama"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="1rpc", chain="kusama"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="1rpc", chain="kusama"}) / sum(ocb:rpc_call:rate_24h{provider="1rpc", chain="kusama"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="1rpc", chain="kusama"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="kusama"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="kusama", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="kusama", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="kusama", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="kusama", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="kusama", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="kusama", region="sgp"}[1h]) diff --git a/benchmarks/oraichain-rpc.yml b/benchmarks/oraichain-rpc.yml index 36fe5517..dd578387 100644 --- a/benchmarks/oraichain-rpc.yml +++ b/benchmarks/oraichain-rpc.yml @@ -107,26 +107,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="orai-official", chain="oraichain", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="orai-official", chain="oraichain", region="sgp"}[1h]) - - - slug: lavenderfive - name: LavenderFive - tag: LavenderFive validator and RPC provider for Cosmos chains - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single Tendermint status POST sent every 60s from 3 regions to LavenderFive's no-key Oraichain endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lavenderfive", chain="oraichain"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="lavenderfive", chain="oraichain"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="lavenderfive", chain="oraichain"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="lavenderfive", chain="oraichain"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="lavenderfive", chain="oraichain"}) / sum(ocb:rpc_call:rate_24h{provider="lavenderfive", chain="oraichain"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="lavenderfive", chain="oraichain"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="lavenderfive", chain="oraichain"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lavenderfive", chain="oraichain", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="lavenderfive", chain="oraichain", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lavenderfive", chain="oraichain", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="lavenderfive", chain="oraichain", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="lavenderfive", chain="oraichain", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="lavenderfive", chain="oraichain", region="sgp"}[1h]) diff --git a/benchmarks/peaq-rpc.yml b/benchmarks/peaq-rpc.yml index 7a7c34f4..792d0ac2 100644 --- a/benchmarks/peaq-rpc.yml +++ b/benchmarks/peaq-rpc.yml @@ -85,29 +85,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="peaq", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="peaq", region="sgp"}[1h]) - - slug: peaq-official - name: Peaq Foundation - tag: Peaq Foundation official public RPC endpoint - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to Peaq Foundation's no-key endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="peaq-official", chain="peaq"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="peaq-official", chain="peaq"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="peaq-official", chain="peaq"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="peaq-official", chain="peaq"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="peaq-official", chain="peaq"}) / sum(ocb:rpc_call:rate_24h{provider="peaq-official", chain="peaq"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="peaq-official", chain="peaq"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="peaq-official", chain="peaq"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="peaq-official", chain="peaq", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="peaq-official", chain="peaq", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="peaq-official", chain="peaq", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="peaq-official", chain="peaq", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="peaq-official", chain="peaq", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="peaq-official", chain="peaq", region="sgp"}[1h]) - - slug: onfinality name: OnFinality tag: OnFinality Substrate public gateway for Peaq diff --git a/benchmarks/songbird-rpc.yml b/benchmarks/songbird-rpc.yml index db727d94..d8207bac 100644 --- a/benchmarks/songbird-rpc.yml +++ b/benchmarks/songbird-rpc.yml @@ -108,29 +108,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="songbird", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="songbird", region="sgp"}[1h]) - - slug: blastapi - name: Blast API - tag: Blast API public gateway for Songbird - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key Songbird endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="songbird"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="songbird"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="songbird"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="songbird"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="songbird"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="songbird"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="songbird"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="songbird"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="songbird", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="songbird", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="songbird", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="songbird", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="songbird", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="songbird", region="sgp"}[1h]) - - slug: ftso-au name: FTSO AU tag: ftso.com.au community Songbird RPC node diff --git a/benchmarks/telos-rpc.yml b/benchmarks/telos-rpc.yml index 53136be7..00c3d939 100644 --- a/benchmarks/telos-rpc.yml +++ b/benchmarks/telos-rpc.yml @@ -107,26 +107,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="telos", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="telos", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for Telos EVM - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key Telos endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="telos"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="telos"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="telos"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="telos"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="telos"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="telos"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="telos"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="telos"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="telos", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="telos", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="telos", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="telos", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="telos", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="telos", region="sgp"}[1h]) diff --git a/benchmarks/warden-rpc.yml b/benchmarks/warden-rpc.yml index 0bb99648..4aa2f76c 100644 --- a/benchmarks/warden-rpc.yml +++ b/benchmarks/warden-rpc.yml @@ -85,29 +85,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="publicnode", chain="warden", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="publicnode", chain="warden", region="sgp"}[1h]) - - slug: drpc - name: dRPC - tag: dRPC public gateway for Warden Protocol - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to dRPC's no-key Warden endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="warden"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="warden"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="warden"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="warden"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="warden"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="warden"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="warden"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="warden"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="warden", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="warden", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="warden", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="warden", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="warden", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="warden", region="sgp"}[1h]) - - slug: warden-official name: Warden Foundation tag: Warden Foundation official public EVM RPC diff --git a/benchmarks/wemix-rpc.yml b/benchmarks/wemix-rpc.yml index 9b3772d5..62f9b5ac 100644 --- a/benchmarks/wemix-rpc.yml +++ b/benchmarks/wemix-rpc.yml @@ -107,49 +107,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="wemix", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="wemix", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for WEMIX 3.0 - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key WEMIX endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="wemix"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="wemix"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="wemix"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="wemix"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="wemix"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="wemix"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="wemix"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="wemix"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="wemix", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="wemix", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="wemix", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="wemix", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="wemix", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="wemix", region="sgp"}[1h]) - - - slug: "1rpc" - name: 1RPC - tag: 1RPC privacy-preserving relay for WEMIX - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to 1RPC's no-key WEMIX endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="wemix"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="1rpc", chain="wemix"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="1rpc", chain="wemix"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="1rpc", chain="wemix"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="1rpc", chain="wemix"}) / sum(ocb:rpc_call:rate_24h{provider="1rpc", chain="wemix"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="1rpc", chain="wemix"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="wemix"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="wemix", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="wemix", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="wemix", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="wemix", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="wemix", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="wemix", region="sgp"}[1h]) diff --git a/benchmarks/zetachain-rpc.yml b/benchmarks/zetachain-rpc.yml index 2b9a1bba..c53c320d 100644 --- a/benchmarks/zetachain-rpc.yml +++ b/benchmarks/zetachain-rpc.yml @@ -153,26 +153,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="allthatnode", chain="zetachain", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="allthatnode", chain="zetachain", region="sgp"}[1h]) - - - slug: blastapi - name: Blast API - tag: Blast API public gateway for ZetaChain mainnet - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Blast API's no-key ZetaChain endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="zetachain"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="zetachain"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="zetachain"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="zetachain"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="zetachain"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="zetachain"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="zetachain"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="zetachain"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="zetachain", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="zetachain", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="zetachain", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="zetachain", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="zetachain", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="zetachain", region="sgp"}[1h]) diff --git a/harnesses/rpc-capabilities/cmd/script/config.go b/harnesses/rpc-capabilities/cmd/script/config.go index ab02e508..325b2daf 100644 --- a/harnesses/rpc-capabilities/cmd/script/config.go +++ b/harnesses/rpc-capabilities/cmd/script/config.go @@ -1339,19 +1339,17 @@ func chains() []Chain { {Slug: "stavr", Name: "STAVR", URL: envDefault("RPC_URL_COMDEX_STAVR", "https://comdex.rpc.m.stavr.tech:443")}, }, }, - // Fantom — EVM (chain 250). 5 keyless providers. + // Fantom — EVM (chain 250). 3 keyless providers. { Slug: "fantom", Name: "Fantom", Providers: []Provider{ {Slug: "fantom-official", Name: "Fantom Foundation", URL: envDefault("RPC_URL_FANTOM_OFFICIAL", "https://rpcapi.fantom.network")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_FANTOM_DRPC", "https://fantom.drpc.org")}, - {Slug: "ankr", Name: "Ankr", URL: envDefault("RPC_URL_FANTOM_ANKR", "https://rpc.ankr.com/fantom")}, {Slug: "thirdweb", Name: "Thirdweb", URL: envDefault("RPC_URL_FANTOM_THIRDWEB", "https://250.rpc.thirdweb.com")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_FANTOM_BLASTAPI", "https://fantom-mainnet.public.blastapi.io")}, }, }, - // Kusama — Polkadot canary relay chain. 6 keyless providers. + // Kusama — Polkadot canary relay chain. 3 keyless providers. { Slug: "kusama", Name: "Kusama", @@ -1360,9 +1358,6 @@ func chains() []Chain { {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_KUSAMA_PUBLICNODE", "https://kusama-rpc.publicnode.com")}, {Slug: "onfinality", Name: "OnFinality", URL: envDefault("RPC_URL_KUSAMA_ONFINALITY", "https://kusama.api.onfinality.io/public")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_KUSAMA_DRPC", "https://kusama.drpc.org")}, - {Slug: "ibp", Name: "IBP Network", URL: envDefault("RPC_URL_KUSAMA_IBP", "https://rpc.ibp.network/kusama")}, - {Slug: "dwellir", Name: "Dwellir", URL: envDefault("RPC_URL_KUSAMA_DWELLIR", "https://kusama-rpc.dwellir.com")}, - {Slug: "1rpc", Name: "1RPC", URL: envDefault("RPC_URL_KUSAMA_1RPC", "https://1rpc.io/ksm")}, }, }, // Hydration — Polkadot DeFi parachain (HydraDX). 5 keyless providers. @@ -1378,7 +1373,7 @@ func chains() []Chain { {Slug: "parachains-network", Name: "Parachains.network", URL: envDefault("RPC_URL_HYDRATION_PARACHAINS", "https://rpc.parachains.network/hydradx")}, }, }, - // ZetaChain — EVM omnichain L1 (chain 7000). 5 keyless providers. + // ZetaChain — EVM omnichain L1 (chain 7000). 4 keyless providers. { Slug: "zetachain", Name: "ZetaChain", @@ -1387,10 +1382,9 @@ func chains() []Chain { {Slug: "blockpi", Name: "BlockPi", URL: envDefault("RPC_URL_ZETACHAIN_BLOCKPI", "https://zetachain.blockpi.network/v1/rpc/public")}, {Slug: "thirdweb", Name: "Thirdweb", URL: envDefault("RPC_URL_ZETACHAIN_THIRDWEB", "https://7000.rpc.thirdweb.com")}, {Slug: "allthatnode", Name: "AllThatNode", URL: envDefault("RPC_URL_ZETACHAIN_ATN", "https://zetachain-mainnet.rpc.public.allthatnode.com")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_ZETACHAIN_BLASTAPI", "https://zetachain-mainnet.public.blastapi.io")}, }, }, - // HAQQ — EVM Islamic finance L1 (chain 11235). 4 keyless providers. + // HAQQ — EVM Islamic finance L1 (chain 11235). 3 keyless providers. { Slug: "haqq", Name: "HAQQ", @@ -1398,21 +1392,18 @@ func chains() []Chain { {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_HAQQ_PUBLICNODE", "https://haqq-evm-rpc.publicnode.com")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_HAQQ_DRPC", "https://haqq.drpc.org")}, {Slug: "haqq-official", Name: "HAQQ Foundation", URL: envDefault("RPC_URL_HAQQ_OFFICIAL", "https://rpc.eth.haqq.network")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_HAQQ_BLASTAPI", "https://haqq-network.public.blastapi.io")}, }, }, - // Etherlink — Tezos EVM L2 (chain 42793). 4 keyless providers. + // Etherlink — Tezos EVM L2 (chain 42793). 2 keyless providers. { Slug: "etherlink", Name: "Etherlink", Providers: []Provider{ {Slug: "etherlink-official", Name: "Etherlink Foundation", URL: envDefault("RPC_URL_ETHERLINK_OFFICIAL", "https://node.mainnet.etherlink.com")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_ETHERLINK_DRPC", "https://etherlink.drpc.org")}, - {Slug: "ankr", Name: "Ankr", URL: envDefault("RPC_URL_ETHERLINK_ANKR", "https://rpc.ankr.com/etherlink")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_ETHERLINK_BLASTAPI", "https://etherlink-mainnet.public.blastapi.io")}, }, }, - // Chiliz — Sports fan token EVM (chain 88888). 4 keyless providers. + // Chiliz — Sports fan token EVM (chain 88888). 3 keyless providers. { Slug: "chiliz", Name: "Chiliz", @@ -1420,39 +1411,34 @@ func chains() []Chain { {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_CHILIZ_PUBLICNODE", "https://chiliz-rpc.publicnode.com")}, {Slug: "ankr", Name: "Ankr", URL: envDefault("RPC_URL_CHILIZ_ANKR", "https://rpc.ankr.com/chiliz")}, {Slug: "chiliz-official", Name: "Chiliz Foundation", URL: envDefault("RPC_URL_CHILIZ_OFFICIAL", "https://rpc.chiliz.com")}, - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_CHILIZ_DRPC", "https://chiliz.drpc.org")}, }, }, - // WEMIX — Korean web3 gaming EVM (chain 1111). 4 keyless providers. + // WEMIX — Korean web3 gaming EVM (chain 1111). 2 keyless providers. { Slug: "wemix", Name: "WEMIX", Providers: []Provider{ - {Slug: "wemix-official", Name: "WEMIX Foundation", URL: envDefault("RPC_URL_WEMIX_OFFICIAL", "https://api.wemix.com")}, + {Slug: "wemix-official", Name: "WeMade", URL: envDefault("RPC_URL_WEMIX_OFFICIAL", "https://api.wemix.com")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_WEMIX_DRPC", "https://wemix.drpc.org")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_WEMIX_BLASTAPI", "https://wemix-mainnet.public.blastapi.io")}, - {Slug: "1rpc", Name: "1RPC", URL: envDefault("RPC_URL_WEMIX_1RPC", "https://1rpc.io/wemix")}, }, }, - // Songbird — Flare canary network (chain 19). 4 keyless providers. + // Songbird — Flare canary network (chain 19). 3 keyless providers. { Slug: "songbird", Name: "Songbird", Providers: []Provider{ {Slug: "flare-official", Name: "Flare Foundation", URL: envDefault("RPC_URL_SONGBIRD_FLARE", "https://songbird-api.flare.network/ext/C/rpc")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_SONGBIRD_DRPC", "https://songbird.drpc.org")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_SONGBIRD_BLASTAPI", "https://songbird-mainnet.public.blastapi.io")}, {Slug: "ftso-au", Name: "FTSO AU", URL: envDefault("RPC_URL_SONGBIRD_FTSOAU", "https://songbird.rpc.ftso.au")}, }, }, - // Cronos zkEVM — zkSync-stack Cronos L2 (chain 388). 3 keyless providers. + // Cronos zkEVM — zkSync-stack Cronos L2 (chain 388). 2 keyless providers. { Slug: "cronos-zkevm", Name: "Cronos zkEVM", Providers: []Provider{ {Slug: "cronos-zkevm-official", Name: "Crypto.com Foundation", URL: envDefault("RPC_URL_CRONOS_ZKEVM_OFFICIAL", "https://mainnet.zkevm.cronos.org")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_CRONOS_ZKEVM_DRPC", "https://cronos-zkevm.drpc.org")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_CRONOS_ZKEVM_BLASTAPI", "https://cronos-zkevm-mainnet.public.blastapi.io")}, }, }, // Ethereum Classic — Original Ethereum chain (chain 61). 3 keyless providers. @@ -1465,14 +1451,13 @@ func chains() []Chain { {Slug: "etcmc", Name: "ETCMC", URL: envDefault("RPC_URL_ETC_ETCMC", "https://etcmc.rpc.nz")}, }, }, - // Telos EVM — High-performance EVM (chain 40). 3 keyless providers. + // Telos EVM — High-performance EVM (chain 40). 2 keyless providers. { Slug: "telos", Name: "Telos", Providers: []Provider{ {Slug: "telos-official", Name: "Telos Foundation", URL: envDefault("RPC_URL_TELOS_OFFICIAL", "https://mainnet.telos.net/evm")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_TELOS_DRPC", "https://telos.drpc.org")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_TELOS_BLASTAPI", "https://telos-mainnet.public.blastapi.io")}, }, }, // PulseChain — Full-state Ethereum fork (chain 369). 3 keyless providers. @@ -1485,17 +1470,16 @@ func chains() []Chain { {Slug: "g4mm4", Name: "G4MM4", URL: envDefault("RPC_URL_PULSECHAIN_G4MM4", "https://rpc-pulsechain.g4mm4.io")}, }, }, - // Warden Protocol — Intent-based EVM L1. 3 keyless providers. + // Warden Protocol — Intent-based EVM L1. 2 keyless providers. { Slug: "warden", Name: "Warden Protocol", Providers: []Provider{ {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_WARDEN_PUBLICNODE", "https://warden-evm-rpc.publicnode.com")}, - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_WARDEN_DRPC", "https://warden.drpc.org")}, {Slug: "warden-official", Name: "Warden Foundation", URL: envDefault("RPC_URL_WARDEN_OFFICIAL", "https://evm.wardenprotocol.org")}, }, }, - // Oraichain — AI-focused Cosmos SDK chain. 3 keyless providers. + // Oraichain — AI-focused Cosmos SDK chain. 2 keyless providers. { Slug: "oraichain", Name: "Oraichain", @@ -1503,17 +1487,15 @@ func chains() []Chain { Providers: []Provider{ {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_ORAICHAIN_PUBLICNODE", "https://oraichain-rpc.publicnode.com")}, {Slug: "orai-official", Name: "Orai Foundation", URL: envDefault("RPC_URL_ORAICHAIN_OFFICIAL", "https://rpc.orai.io")}, - {Slug: "lavenderfive", Name: "LavenderFive", URL: envDefault("RPC_URL_ORAICHAIN_LAVENDERFIVE", "https://oraichain-rpc.lavenderfive.com")}, }, }, - // Peaq Network — DePIN Polkadot parachain. 3 keyless providers. + // Peaq Network — DePIN Polkadot parachain. 2 keyless providers. { Slug: "peaq", Name: "Peaq Network", Kind: "polkadot", Providers: []Provider{ {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_PEAQ_PUBLICNODE", "https://peaq-rpc.publicnode.com")}, - {Slug: "peaq-official", Name: "Peaq Foundation", URL: envDefault("RPC_URL_PEAQ_OFFICIAL", "https://rpc.peaq.network")}, {Slug: "onfinality", Name: "OnFinality", URL: envDefault("RPC_URL_PEAQ_ONFINALITY", "https://peaq.api.onfinality.io/public")}, }, }, From 0caf2dcf6700bcc17764cd8b9bec6ad1a5ad468e Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:48:49 +0200 Subject: [PATCH 03/39] feat: add wave 7 chains to /chains hub (fantom, kusama, hydration, zetachain, haqq, etherlink, chiliz, wemix, songbird, etc-classic, telos, pulsechain, warden, oraichain, peaq) --- src/lib/chains.ts | 135 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/src/lib/chains.ts b/src/lib/chains.ts index d1abd7e9..aa8c14eb 100644 --- a/src/lib/chains.ts +++ b/src/lib/chains.ts @@ -951,6 +951,141 @@ export const CHAINS: ChainEntry[] = [ description: "Cosmos SDK DeFi chain (comdex-1) providing decentralised synthetics and lending with on-chain commodity markets, CMDX staking token, CometBFT consensus, IBC-connected to Osmosis and Cosmos Hub.", }, + { + slug: "fantom", + label: "Fantom", + category: "L1", + nativeSymbol: "FTM", + website: "https://fantom.foundation", + description: + "DAG-based EVM-compatible L1 (chain 250) using Lachesis aBFT consensus for ~1-2 second finality, FTM as native gas token. Home to a rich DeFi ecosystem including SpookySwap and Beethoven X.", + }, + { + slug: "kusama", + label: "Kusama", + category: "L1", + nativeSymbol: "KSM", + website: "https://kusama.network", + description: + "Polkadot canary relay chain providing a fast-paced environment for experimentation with parachain slots, shared security and on-chain governance before features graduate to Polkadot mainnet.", + }, + { + slug: "hydration", + label: "Hydration", + category: "L2", + nativeSymbol: "HDX", + website: "https://hydration.net", + description: + "Polkadot parachain DeFi hub (formerly HydraDX) featuring an Omnipool AMM, Stableswap, money market, and OTC desk, secured by Polkadot shared security with HDX as the native governance token.", + }, + { + slug: "zetachain", + label: "ZetaChain", + category: "L1", + nativeSymbol: "ZETA", + website: "https://www.zetachain.com", + description: + "EVM omnichain L1 (chain 7000) with native cross-chain messaging and value transfer across Bitcoin, Ethereum, BNB Chain, and other networks without bridges, using ZETA as the gas and bonding token.", + }, + { + slug: "haqq", + label: "HAQQ", + category: "L1", + nativeSymbol: "ISLM", + website: "https://haqq.network", + description: + "EVM-compatible L1 (chain 11235) designed for Shariah-compliant Islamic finance applications, powered by the Ethermint stack with ISLM as the native token and CometBFT consensus.", + }, + { + slug: "etherlink", + label: "Etherlink", + category: "L2", + nativeSymbol: "XTZ", + website: "https://www.etherlink.com", + description: + "EVM-compatible L2 (chain 42793) built on Tezos Smart Rollup technology, offering decentralised sequencing and fast finality secured by Tezos bakers, using XTZ as the native gas token.", + }, + { + slug: "chiliz", + label: "Chiliz", + category: "L1", + nativeSymbol: "CHZ", + website: "https://www.chiliz.com", + description: + "EVM-compatible sports and entertainment blockchain (chain 88888, Chiliz Chain 2.0) enabling sports fan tokens and NFTs for football clubs and leagues worldwide, with CHZ as the native gas token.", + }, + { + slug: "wemix", + label: "WEMIX", + category: "L1", + nativeSymbol: "WEMIX", + website: "https://wemix.com", + description: + "EVM-compatible Web3 gaming blockchain (chain 1111, WEMIX 3.0) by WeMade, home to P2E gaming titles like MirrorWorld and WonderX across the South Korean gaming ecosystem.", + }, + { + slug: "songbird", + label: "Songbird", + category: "L1", + nativeSymbol: "SGB", + website: "https://flare.network/songbird", + description: + "Canary network for Flare (chain 19), EVM-compatible with a native FTSO data oracle system, used to test governance proposals and smart contract upgrades before deployment to Flare mainnet.", + }, + { + slug: "ethereum-classic", + label: "Ethereum Classic", + category: "L1", + nativeSymbol: "ETC", + website: "https://ethereumclassic.org", + description: + "The original unforked Ethereum chain (chain 61) continuing after the 2016 DAO hack, committed to immutability and proof-of-work consensus with ETC as the native currency.", + }, + { + slug: "telos", + label: "Telos", + category: "L1", + nativeSymbol: "TLOS", + website: "https://www.telos.net", + description: + "High-performance EVM blockchain (chain 40) built on EOSIO technology delivering fast block times and zero front-running via fixed-order transaction processing, with TLOS as the native token.", + }, + { + slug: "pulsechain", + label: "PulseChain", + category: "L1", + nativeSymbol: "PLS", + website: "https://pulsechain.com", + description: + "Full-state Ethereum fork (chain 369) launched in 2023 by Richard Heart, carrying a copy of all Ethereum state at genesis with PLS as the native gas token and a Proof-of-Stake consensus.", + }, + { + slug: "warden", + label: "Warden Protocol", + category: "L1", + nativeSymbol: "WARD", + website: "https://wardenprotocol.org", + description: + "Intent-based modular L1 built with Cosmos SDK, enabling omnichain smart accounts (Spaces) with programmable key management and cross-chain transaction orchestration via intents.", + }, + { + slug: "oraichain", + label: "Oraichain", + category: "L1", + nativeSymbol: "ORAI", + website: "https://orai.io", + description: + "AI-focused Cosmos SDK blockchain (Oraichain mainnet) providing verifiable AI execution via a decentralised oracle layer, CosmWasm smart contracts, and IBC connectivity across the Cosmos ecosystem.", + }, + { + slug: "peaq", + label: "Peaq", + category: "L2", + nativeSymbol: "PEAQ", + website: "https://www.peaq.network", + description: + "Polkadot parachain purpose-built for DePIN (Decentralised Physical Infrastructure Networks) with machine identity, machine staking, and role-based access control primitives for IoT and mobility networks.", + }, ]; export const CHAIN_BY_SLUG = new Map(CHAINS.map((c) => [c.slug, c])); From 3f162e7a960653250c7bab62fa9cf5d562f42234 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:49:21 +0200 Subject: [PATCH 04/39] feat: add 15 wave 7 chains to /chains hub (#1790) * fix: remove dead providers (BlastAPI shutdown, Ankr key-gated, dRPC/1RPC not supported) * feat: add wave 7 chains to /chains hub (fantom, kusama, hydration, zetachain, haqq, etherlink, chiliz, wemix, songbird, etc-classic, telos, pulsechain, warden, oraichain, peaq) --- src/lib/chains.ts | 135 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/src/lib/chains.ts b/src/lib/chains.ts index d1abd7e9..aa8c14eb 100644 --- a/src/lib/chains.ts +++ b/src/lib/chains.ts @@ -951,6 +951,141 @@ export const CHAINS: ChainEntry[] = [ description: "Cosmos SDK DeFi chain (comdex-1) providing decentralised synthetics and lending with on-chain commodity markets, CMDX staking token, CometBFT consensus, IBC-connected to Osmosis and Cosmos Hub.", }, + { + slug: "fantom", + label: "Fantom", + category: "L1", + nativeSymbol: "FTM", + website: "https://fantom.foundation", + description: + "DAG-based EVM-compatible L1 (chain 250) using Lachesis aBFT consensus for ~1-2 second finality, FTM as native gas token. Home to a rich DeFi ecosystem including SpookySwap and Beethoven X.", + }, + { + slug: "kusama", + label: "Kusama", + category: "L1", + nativeSymbol: "KSM", + website: "https://kusama.network", + description: + "Polkadot canary relay chain providing a fast-paced environment for experimentation with parachain slots, shared security and on-chain governance before features graduate to Polkadot mainnet.", + }, + { + slug: "hydration", + label: "Hydration", + category: "L2", + nativeSymbol: "HDX", + website: "https://hydration.net", + description: + "Polkadot parachain DeFi hub (formerly HydraDX) featuring an Omnipool AMM, Stableswap, money market, and OTC desk, secured by Polkadot shared security with HDX as the native governance token.", + }, + { + slug: "zetachain", + label: "ZetaChain", + category: "L1", + nativeSymbol: "ZETA", + website: "https://www.zetachain.com", + description: + "EVM omnichain L1 (chain 7000) with native cross-chain messaging and value transfer across Bitcoin, Ethereum, BNB Chain, and other networks without bridges, using ZETA as the gas and bonding token.", + }, + { + slug: "haqq", + label: "HAQQ", + category: "L1", + nativeSymbol: "ISLM", + website: "https://haqq.network", + description: + "EVM-compatible L1 (chain 11235) designed for Shariah-compliant Islamic finance applications, powered by the Ethermint stack with ISLM as the native token and CometBFT consensus.", + }, + { + slug: "etherlink", + label: "Etherlink", + category: "L2", + nativeSymbol: "XTZ", + website: "https://www.etherlink.com", + description: + "EVM-compatible L2 (chain 42793) built on Tezos Smart Rollup technology, offering decentralised sequencing and fast finality secured by Tezos bakers, using XTZ as the native gas token.", + }, + { + slug: "chiliz", + label: "Chiliz", + category: "L1", + nativeSymbol: "CHZ", + website: "https://www.chiliz.com", + description: + "EVM-compatible sports and entertainment blockchain (chain 88888, Chiliz Chain 2.0) enabling sports fan tokens and NFTs for football clubs and leagues worldwide, with CHZ as the native gas token.", + }, + { + slug: "wemix", + label: "WEMIX", + category: "L1", + nativeSymbol: "WEMIX", + website: "https://wemix.com", + description: + "EVM-compatible Web3 gaming blockchain (chain 1111, WEMIX 3.0) by WeMade, home to P2E gaming titles like MirrorWorld and WonderX across the South Korean gaming ecosystem.", + }, + { + slug: "songbird", + label: "Songbird", + category: "L1", + nativeSymbol: "SGB", + website: "https://flare.network/songbird", + description: + "Canary network for Flare (chain 19), EVM-compatible with a native FTSO data oracle system, used to test governance proposals and smart contract upgrades before deployment to Flare mainnet.", + }, + { + slug: "ethereum-classic", + label: "Ethereum Classic", + category: "L1", + nativeSymbol: "ETC", + website: "https://ethereumclassic.org", + description: + "The original unforked Ethereum chain (chain 61) continuing after the 2016 DAO hack, committed to immutability and proof-of-work consensus with ETC as the native currency.", + }, + { + slug: "telos", + label: "Telos", + category: "L1", + nativeSymbol: "TLOS", + website: "https://www.telos.net", + description: + "High-performance EVM blockchain (chain 40) built on EOSIO technology delivering fast block times and zero front-running via fixed-order transaction processing, with TLOS as the native token.", + }, + { + slug: "pulsechain", + label: "PulseChain", + category: "L1", + nativeSymbol: "PLS", + website: "https://pulsechain.com", + description: + "Full-state Ethereum fork (chain 369) launched in 2023 by Richard Heart, carrying a copy of all Ethereum state at genesis with PLS as the native gas token and a Proof-of-Stake consensus.", + }, + { + slug: "warden", + label: "Warden Protocol", + category: "L1", + nativeSymbol: "WARD", + website: "https://wardenprotocol.org", + description: + "Intent-based modular L1 built with Cosmos SDK, enabling omnichain smart accounts (Spaces) with programmable key management and cross-chain transaction orchestration via intents.", + }, + { + slug: "oraichain", + label: "Oraichain", + category: "L1", + nativeSymbol: "ORAI", + website: "https://orai.io", + description: + "AI-focused Cosmos SDK blockchain (Oraichain mainnet) providing verifiable AI execution via a decentralised oracle layer, CosmWasm smart contracts, and IBC connectivity across the Cosmos ecosystem.", + }, + { + slug: "peaq", + label: "Peaq", + category: "L2", + nativeSymbol: "PEAQ", + website: "https://www.peaq.network", + description: + "Polkadot parachain purpose-built for DePIN (Decentralised Physical Infrastructure Networks) with machine identity, machine staking, and role-based access control primitives for IoT and mobility networks.", + }, ]; export const CHAIN_BY_SLUG = new Map(CHAINS.map((c) => [c.slug, c])); From a0ef2373e2415fc625a9314a539f178209e380e8 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:57:39 +0200 Subject: [PATCH 05/39] fix: remove drift and variational from perp venue meta (404 on /perp redirect) --- src/lib/perp-venue-context.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib/perp-venue-context.ts b/src/lib/perp-venue-context.ts index 28a396b7..bb7edf98 100644 --- a/src/lib/perp-venue-context.ts +++ b/src/lib/perp-venue-context.ts @@ -31,7 +31,6 @@ export const PERP_VENUE_META: Record< "gmx-v2": { url: "https://gmx.io", chainLabel: "Arbitrum", productSlug: "gmx" }, gains: { url: "https://gains.trade", chainLabel: "Arbitrum, Base, Polygon, ApeChain" }, dydx: { url: "https://dydx.trade", chainLabel: "Cosmos" }, - drift: { url: "https://drift.trade", chainLabel: "Solana" }, vertex: { url: "https://vertexprotocol.com", chainLabel: "Arbitrum" }, paradex: { url: "https://paradex.trade", chainLabel: "Starknet" }, aster: { url: "https://asterdex.com", chainLabel: "BNB Chain" }, @@ -39,7 +38,6 @@ export const PERP_VENUE_META: Record< extended: { url: "https://extended.exchange", chainLabel: "Starknet" }, aevo: { url: "https://aevo.xyz", chainLabel: "OP Stack" }, pacifica: { url: "https://pacifica.fi", chainLabel: "Solana" }, - variational: { url: "https://variational.io", chainLabel: "Arbitrum" }, ostium: { url: "https://ostium.app", chainLabel: "Arbitrum" }, grvt: { url: "https://grvt.io", chainLabel: "zkSync" }, polymarket: { url: "https://polymarket.com", chainLabel: "Polygon" }, From a68392d9b0900fdc4a3a132f3c84de88fbc28b3f Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:07:45 +0200 Subject: [PATCH 06/39] fix: filter non-provider result links in answers, add bridge to footer --- src/app/answers/[slug]/page.tsx | 7 +++++++ src/components/site-footer.tsx | 1 + 2 files changed, 8 insertions(+) diff --git a/src/app/answers/[slug]/page.tsx b/src/app/answers/[slug]/page.tsx index 28626486..cdd7fe68 100644 --- a/src/app/answers/[slug]/page.tsx +++ b/src/app/answers/[slug]/page.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation"; import Link from "next/link"; import { ArrowLeft, ArrowUpRight } from "lucide-react"; import { loadAnswer, loadAllAnswers } from "@/lib/answers"; +import { getProviderSlugs } from "@/lib/providers"; import { renderTemplate } from "@/lib/bench-template"; import { cleanLeftoverTokens, @@ -155,8 +156,14 @@ export default async function AnswerPage({ // Top results from the referenced bench, mirroring the alternatives // top-N section: surfaces the answer visually for skim readers + gives // crawlers concrete provider names + numbers in the SSR HTML. + // Only include results whose slug has a matching /products/ page — some + // benches (e.g. pm-resolution-delay) use non-provider slugs like + // "politics" or "all-markets" as result labels, which would otherwise + // generate 404 links. + const validProviderSlugs = new Set(await getProviderSlugs()); const sortedResults = [...bench.results] .filter((r) => !isRegion(r.slug)) + .filter((r) => validProviderSlugs.has(r.slug)) .filter((r) => r.ms.p50 !== 0 || r.ms.p99 !== 0) .sort((a, b) => bench.higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50, diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx index c094d91b..b338facb 100644 --- a/src/components/site-footer.tsx +++ b/src/components/site-footer.tsx @@ -34,6 +34,7 @@ export function SiteFooter() { { label: "RPC", href: "/rpc" }, { label: "Data APIs", href: "/data-api" }, { label: "Perpetuals", href: "/perps" }, + { label: "Bridge", href: "/bridge" }, { label: "Compare", href: "/compare" }, { label: "Alternatives", href: "/alternatives" }, { label: "Answers", href: "/answers" }, From 6af4b14650648c1e13ace09e182a6863db5d462f Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:28:49 +0200 Subject: [PATCH 07/39] fix: drop dead RPC providers across 14 chains (DNS failures, 404s, paid-plan blocks) --- benchmarks/astar-rpc.yml | 60 +------- benchmarks/aurora-rpc.yml | 61 +------- benchmarks/ethereum-rpc.yml | 42 ++---- benchmarks/hydration-rpc.yml | 142 ++---------------- benchmarks/kaia-rpc.yml | 38 +---- benchmarks/moonriver-rpc.yml | 38 +---- benchmarks/ronin-rpc.yml | 39 +---- benchmarks/sei-rpc.yml | 39 +---- benchmarks/songbird-rpc.yml | 61 +------- benchmarks/telos-rpc.yml | 39 +---- benchmarks/tron-rpc.yml | 47 ++---- benchmarks/warden-rpc.yml | 14 +- benchmarks/zetachain-rpc.yml | 84 +---------- .../rpc-capabilities/cmd/script/config.go | 121 ++++++--------- 14 files changed, 159 insertions(+), 666 deletions(-) diff --git a/benchmarks/astar-rpc.yml b/benchmarks/astar-rpc.yml index 6f1a95f6..495c8ea5 100644 --- a/benchmarks/astar-rpc.yml +++ b/benchmarks/astar-rpc.yml @@ -4,7 +4,7 @@ slug: astar-rpc number: "154" title: Fastest free Astar Network RPC, live no-key endpoint latency seo_title: "Fastest free Astar Network RPC 2026" -seo_description: "{{best_name}} leads free Astar Network RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 4 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Astar Network RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 2 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public Astar Network RPC endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,13 +14,13 @@ unit: ms higher_is_better: false seo_intro: | - Astar Network (chain 592) is Polkadot's flagship EVM and WebAssembly parachain, ASTR gas token, supporting both Solidity and ink! smart contracts. Developed by Startale (formerly Stake Technologies) and chosen by Sony for Soneium infrastructure, it exposes a standard EVM endpoint alongside Substrate RPC. The Foundation's evm.astar.network, BlastAPI, and OnFinality provide keyless access probed every 60 seconds from three regions. + Astar Network (chain 592) is Polkadot's flagship EVM and WebAssembly parachain, ASTR gas token, supporting both Solidity and ink! smart contracts. Developed by Startale (formerly Stake Technologies) and chosen by Sony for Soneium infrastructure, it exposes a standard EVM endpoint alongside Substrate RPC. OnFinality and 1RPC provide keyless access probed every 60 seconds from three regions. PublicNode returned 404 and BlastAPI's DNS is dead. abstract: | Per-chain member of the RPC latency cluster, extended to Astar Network. We measure the round-trip latency of a single, identical RPC call (`eth_getBlockByNumber`) against every no-key public Astar Network endpoint - that sustains continuous probing, 4 providers at launch, + that sustains continuous probing, 2 providers, every 60 seconds, from us-east, eu-west and Singapore. The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) so the leaderboard rewards sustained availability rather than @@ -34,16 +34,16 @@ methodology: - "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` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"astar\"`. Provider coverage at launch: 4 no-key endpoints (Astar Foundation, BlastAPI, OnFinality, 1RPC)." + - "Chain scope: every query on this page is pinned to `chain=\"astar\"`. Provider coverage: 2 no-key endpoints (OnFinality, 1RPC). PublicNode returned 404; BlastAPI DNS is dead." findings: - - "{{best_name}} currently leads free Astar Network RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 4 measured providers." + - "{{best_name}} currently leads free Astar Network RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 2 measured providers." faq: - q: "What is the fastest free Astar Network RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 4 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 2 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." - q: "Which Astar Network RPCs work without an API key?" - a: "4 endpoints sustain continuous keyless probing at launch: Astar Foundation, BlastAPI, OnFinality, 1RPC. Every listed endpoint was live-verified before inclusion." + a: "2 endpoints sustain continuous keyless probing: OnFinality and 1RPC. PublicNode returned 404 and BlastAPI's DNS is dead; both were removed." - q: "Does the fastest Astar Network RPC change by region?" a: "Often. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate." - q: "How is Astar Network RPC latency measured here?" @@ -65,52 +65,6 @@ dimensions: - { value: sgp, label: Singapore } providers: - - slug: astar-official - name: Astar Foundation - tag: Astar Foundation's official EVM RPC - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Astar Foundation's no-key Astar Network endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="astar-official", chain="astar"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="astar-official", chain="astar"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="astar-official", chain="astar"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="astar-official", chain="astar"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="astar-official", chain="astar"}) / sum(ocb:rpc_call:rate_24h{provider="astar-official", chain="astar"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="astar-official", chain="astar"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="astar-official", chain="astar"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="astar-official", chain="astar", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="astar-official", chain="astar", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="astar-official", chain="astar", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="astar-official", chain="astar", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="astar-official", chain="astar", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="astar-official", chain="astar", region="sgp"}[1h]) - - - slug: blastapi - name: BlastAPI - tag: BlastAPI public endpoint - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to BlastAPI's no-key Astar Network endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="astar"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blastapi", chain="astar"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blastapi", chain="astar"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blastapi", chain="astar"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blastapi", chain="astar"}) / sum(ocb:rpc_call:rate_24h{provider="blastapi", chain="astar"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blastapi", chain="astar"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="astar"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="astar", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="astar", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="astar", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="astar", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blastapi", chain="astar", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blastapi", chain="astar", region="sgp"}[1h]) - - slug: onfinality name: OnFinality tag: OnFinality shared node service diff --git a/benchmarks/aurora-rpc.yml b/benchmarks/aurora-rpc.yml index e9098f69..e40060b5 100644 --- a/benchmarks/aurora-rpc.yml +++ b/benchmarks/aurora-rpc.yml @@ -4,7 +4,7 @@ slug: aurora-rpc number: "147" title: Fastest free Aurora RPC, live no-key endpoint latency seo_title: "Fastest free Aurora RPC 2026" -seo_description: "{{best_name}} leads free Aurora RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 3 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Aurora RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 1 no-key provider measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public Aurora RPC endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,13 +14,13 @@ unit: ms higher_is_better: false seo_intro: | - Aurora is the EVM runtime on NEAR Protocol (chain 1313161554), exposing full Ethereum tooling to NEAR's sharded execution environment. The official endpoint (`mainnet.aurora.dev`) is operated by Aurora Labs with dRPC and Ankr providing additional keyless alternatives. Every provider is live-verified before inclusion. + Aurora is the EVM runtime on NEAR Protocol (chain 1313161554), exposing full Ethereum tooling to NEAR's sharded execution environment. The official endpoint (`mainnet.aurora.dev`) is operated by Aurora Labs and is the only keyless endpoint sustaining continuous probing. Ankr and dRPC require an API key for Aurora and have been removed. abstract: | Per-chain member of the RPC latency cluster, extended to Aurora. We measure the round-trip latency of a single, identical RPC call - (`eth_getBlockByNumber`) against every no-key public Aurora endpoint - that sustains continuous probing, 3 providers at launch, + (`eth_getBlockByNumber`) against the no-key public Aurora endpoint + that sustains continuous probing (1 provider: Aurora Labs official), every 60 seconds, from us-east, eu-west and Singapore. The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) so the leaderboard rewards sustained availability rather than @@ -34,16 +34,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"aurora\"`. Provider coverage at launch: 3 no-key endpoints (Aurora Official, dRPC, Ankr)." + - "Chain scope: every query on this page is pinned to `chain=\"aurora\"`. Provider coverage: 1 no-key endpoint (Aurora Official). Ankr and dRPC require an API key for Aurora and have been removed." findings: - - "{{best_name}} currently leads free Aurora RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers." + - "{{best_name}} currently leads free Aurora RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) — 1 measured provider." faq: - q: "What is the fastest free Aurora RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 1 no-key provider probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." - q: "Which Aurora RPCs work without an API key?" - a: "3 endpoints sustain continuous keyless probing at launch: Aurora Official, dRPC, Ankr. Every listed endpoint was live-verified before inclusion." + a: "1 endpoint sustains continuous keyless probing: Aurora Official (mainnet.aurora.dev). Ankr returns 403 (API key required) and dRPC returns 403 (API key required) for Aurora." - q: "Does the fastest Aurora RPC change by region?" a: "Often. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate." - q: "How is Aurora RPC latency measured here?" @@ -88,48 +88,3 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="aurora-official", chain="aurora", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="aurora-official", chain="aurora", region="sgp"}[1h]) - - slug: drpc - name: dRPC - tag: Decentralized RPC mesh, consensus-checked - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to dRPC's no-key Aurora endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="aurora"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="aurora"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="aurora"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="aurora"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="aurora"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="aurora"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="aurora"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="aurora"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="aurora", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="aurora", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="aurora", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="aurora", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="aurora", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="aurora", region="sgp"}[1h]) - - - slug: ankr - name: Ankr - tag: Multi-cloud RPC network, 40+ chains, keyless tier - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Ankr's no-key Aurora endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="aurora"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="ankr", chain="aurora"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="ankr", chain="aurora"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="ankr", chain="aurora"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="ankr", chain="aurora"}) / sum(ocb:rpc_call:rate_24h{provider="ankr", chain="aurora"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="ankr", chain="aurora"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="aurora"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="aurora", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="aurora", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="aurora", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="aurora", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="aurora", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="aurora", region="sgp"}[1h]) diff --git a/benchmarks/ethereum-rpc.yml b/benchmarks/ethereum-rpc.yml index 1136e155..ce4e2174 100644 --- a/benchmarks/ethereum-rpc.yml +++ b/benchmarks/ethereum-rpc.yml @@ -4,7 +4,7 @@ slug: ethereum-rpc number: "044" title: Fastest free Ethereum RPC, live no-key endpoint latency seo_title: "Fastest free Ethereum RPC 2026" -seo_description: "{{best_name}} leads free Ethereum RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 7 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Ethereum RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 6 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public Ethereum RPC endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,10 +14,10 @@ unit: ms higher_is_better: false seo_intro: | - Ethereum carries the largest free-RPC cohort we measure: 7 no-key public-read providers answering the same `eth_getBlockByNumber` probe every 60 seconds from three regions. This bench is scoped to public, read-optimised gateways only; MEV-protection RPCs (Flashbots, MEV Blocker, Blink, bloXroute) share the JSON-RPC surface but serve a fundamentally different SLA (send-tx privacy, not read speed) and live in their own cluster on [mev-protect-rpc](/benchmarks/mev-protect-rpc). Cloudflare-eth is the resident cautionary tale of this cohort: sub-second HTTP 200s that increasingly carry a JSON-RPC error body (`-32046 Cannot fulfill request`), and Merkle is excluded outright after recurring Cloudflare lockouts that froze our probes for 20 minutes after a single request. If you paste a free RPC URL into an Ethereum dapp's read path, this page is the live answer to which one deserves it. + Ethereum carries one of the largest free-RPC cohorts we measure: 6 no-key public-read providers answering the same `eth_getBlockByNumber` probe every 60 seconds from three regions. This bench is scoped to public, read-optimised gateways only; MEV-protection RPCs (Flashbots, MEV Blocker, Blink, bloXroute) share the JSON-RPC surface but serve a fundamentally different SLA (send-tx privacy, not read speed) and live in their own cluster on [mev-protect-rpc](/benchmarks/mev-protect-rpc). Cloudflare-eth has been removed from this bench after persistent JSON-RPC error -32603, and Merkle is excluded outright after recurring Cloudflare lockouts that froze our probes for 20 minutes after a single request. If you paste a free RPC URL into an Ethereum dapp's read path, this page is the live answer to which one deserves it. abstract: | - Per-chain member of the public-read RPC latency cluster. We measure the round-trip latency of a single, identical RPC call (`eth_getBlockByNumber`) against every no-key public-read Ethereum endpoint that sustains continuous probing, 7 providers, every 60 seconds, from us-east, eu-west and Singapore. MEV-protection endpoints (private-mempool, anti-sandwich, PBS relays) are measured on the same probe but scored separately on [mev-protect-rpc](/benchmarks/mev-protect-rpc) because their SLA optimises for send-tx privacy rather than read latency, and mixing them here would flatter or penalise them on the wrong axis. The harness also classifies every response (ok / http_err / jsonrpc_err / stale / timeout) and audits archive depth every 5 minutes, so the leaderboard rewards sustained, honest availability rather than a fast error message. The cross-chain view lives on the parent rpc-capabilities benchmark; this page is the Ethereum-scoped answer with per-region breakdowns as a first-class dimension. + Per-chain member of the public-read RPC latency cluster. We measure the round-trip latency of a single, identical RPC call (`eth_getBlockByNumber`) against every no-key public-read Ethereum endpoint that sustains continuous probing, 6 providers, every 60 seconds, from us-east, eu-west and Singapore. MEV-protection endpoints (private-mempool, anti-sandwich, PBS relays) are measured on the same probe but scored separately on [mev-protect-rpc](/benchmarks/mev-protect-rpc) because their SLA optimises for send-tx privacy rather than read latency, and mixing them here would flatter or penalise them on the wrong axis. The harness also classifies every response (ok / http_err / jsonrpc_err / stale / timeout) and audits archive depth every 5 minutes, so the leaderboard rewards sustained, honest availability rather than a fast error message. The cross-chain view lives on the parent rpc-capabilities benchmark; this page is the Ethereum-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." @@ -26,24 +26,24 @@ methodology: - "Call-result classification: `ok` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal." - "Archive depth: every 5 minutes we issue `eth_getBalance` at (head − depth) for depths from Geth's default pruned cap up to 5M blocks, exposing which free endpoints actually serve historical state." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, methodology and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to chain=\"ethereum\". Provider coverage: 7 no-key public-read endpoints (PublicNode, dRPC, Tenderly, Nodies, Lava, MeowRPC, Cloudflare). MEV-protection RPCs (Flashbots, MEV Blocker, Blink, bloXroute) share this probe methodology but score on [mev-protect-rpc](/benchmarks/mev-protect-rpc), because their SLA optimises for send-tx privacy, not read speed." + - "Chain scope: every query on this page is pinned to chain=\"ethereum\". Provider coverage: 6 no-key public-read endpoints (PublicNode, dRPC, Tenderly, Nodies, Lava, MeowRPC). Cloudflare removed (persistent JSON-RPC -32603). MEV-protection RPCs (Flashbots, MEV Blocker, Blink, bloXroute) share this probe methodology but score on [mev-protect-rpc](/benchmarks/mev-protect-rpc), because their SLA optimises for send-tx privacy, not read speed." findings: - - "{{best_name}} currently leads the free Ethereum RPC field at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 7 measured public-read providers, the largest cohort of any chain in the cluster." - - "Cloudflare-eth is the resident cautionary tale: sub-second HTTP 200s that increasingly carry a JSON-RPC error instead of a block number. The success-rate column, not the latency column, tells the real story." + - "{{best_name}} currently leads the free Ethereum RPC field at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 6 measured public-read providers." + - "Cloudflare-eth has been removed from the active cohort: persistent JSON-RPC error -32603 means the endpoint no longer returns usable results." - "Merkle is excluded on Ethereum by design: its endpoint sits behind an aggressive bot filter that locks out programmatic clients for ~20 minutes after one request, invisible on any status page." faq: - q: "What is the fastest free Ethereum RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 7 no-key public-read providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment. If you need MEV-protection endpoints instead of raw read speed, see [mev-protect-rpc](/benchmarks/mev-protect-rpc)." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 6 no-key public-read providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment. If you need MEV-protection endpoints instead of raw read speed, see [mev-protect-rpc](/benchmarks/mev-protect-rpc)." - q: "Which Ethereum RPCs work without an API key?" - a: "The 7 public-read providers on this page: PublicNode, dRPC, Tenderly, Nodies, Lava, MeowRPC, Cloudflare. Every (provider, chain) pair was live-verified no-key before inclusion, and anything that key-gates, region-blocks or rate-limits below our 60-second probe cadence is excluded rather than listed with an asterisk. MEV-protection RPCs (Flashbots, MEV Blocker, Blink, bloXroute) also expose no-key JSON-RPC but serve a different SLA and live on [mev-protect-rpc](/benchmarks/mev-protect-rpc)." + a: "The 6 public-read providers on this page: PublicNode, dRPC, Tenderly, Nodies, Lava, MeowRPC. Every (provider, chain) pair was live-verified no-key before inclusion, and anything that key-gates, region-blocks or rate-limits below our 60-second probe cadence is excluded rather than listed with an asterisk. Cloudflare was removed after persistent JSON-RPC error -32603. MEV-protection RPCs (Flashbots, MEV Blocker, Blink, bloXroute) also expose no-key JSON-RPC but serve a different SLA and live on [mev-protect-rpc](/benchmarks/mev-protect-rpc)." - q: "Does the fastest Ethereum RPC change by region?" a: "Frequently. The headline number averages three probe origins (us-east, eu-west, Singapore), but per-region leaders regularly diverge, a gateway that wins from Virginia can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number on the page to a single origin; pick the one closest to where your requests actually originate." - q: "How is Ethereum RPC latency measured here?" a: "One identical JSON-RPC POST (`eth_getBlockByNumber`) every 60 seconds against each provider from each of 3 regions, with the same plain HTTP client. Wall-clock round-trip is recorded at millisecond precision; p50/p90/p99 are computed via Prometheus `quantile_over_time` over 24 hours. Responses are classified (`ok` / `http_err` / `jsonrpc_err` / `stale` / `timeout`) so an endpoint stuck on an old head or returning errors behind HTTP 200 is never ranked as fastest. The harness is open source and every number on this page is a public Prometheus query you can run yourself." - - q: "Why is Cloudflare's Ethereum RPC marked unreliable here?" - a: "Cloudflare's public Ethereum gateway switched to a permissioned mode for many JSON-RPC methods. The endpoint still responds fast with HTTP 200, but the body is increasingly a JSON-RPC error (`-32046`) rather than a usable result. We classify a call as `ok` only when the HTTP status is 200 AND the body carries a usable `result` field, so Cloudflare's real success rate is visible in the reliability column instead of hiding behind fast error responses." + - q: "Why was Cloudflare's Ethereum RPC removed?" + a: "Cloudflare's public Ethereum gateway returned persistent JSON-RPC error -32603 on our probes. The endpoint responds with HTTP 200 but the body carries an error body rather than a usable result, which means it no longer qualifies as a functional free RPC. We classify a call as `ok` only when the HTTP status is 200 AND the body carries a usable `result` field." source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities @@ -202,26 +202,4 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="meowrpc", chain="ethereum", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="meowrpc", chain="ethereum", region="sgp"}[1h]) - - slug: cloudflare - name: Cloudflare - tag: Permissioned-mode for many JSON-RPC methods - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to Cloudflare's no-key Ethereum endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="cloudflare", chain="ethereum"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="cloudflare", chain="ethereum"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="cloudflare", chain="ethereum"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="cloudflare", chain="ethereum"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="cloudflare", chain="ethereum"}) / sum(ocb:rpc_call:rate_24h{provider="cloudflare", chain="ethereum"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="cloudflare", chain="ethereum"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="cloudflare", chain="ethereum"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="cloudflare", chain="ethereum", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="cloudflare", chain="ethereum", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="cloudflare", chain="ethereum", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="cloudflare", chain="ethereum", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="cloudflare", chain="ethereum", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="cloudflare", chain="ethereum", region="sgp"}[1h]) diff --git a/benchmarks/hydration-rpc.yml b/benchmarks/hydration-rpc.yml index 423f4894..9fca3db1 100644 --- a/benchmarks/hydration-rpc.yml +++ b/benchmarks/hydration-rpc.yml @@ -4,26 +4,23 @@ slug: hydration-rpc number: "186" title: Fastest free Hydration RPC, live no-key endpoint latency seo_title: "Fastest free Hydration RPC 2026" -seo_description: "{{best_name}} leads free Hydration RPC at {{best_p50}} (chain_getHeader p50, 24h). 5 no-key providers measured every 60s from 3 regions." +seo_description: "Hydration RPC bench temporarily suspended: all 5 providers are DNS dead or returning HTTP 400. Will resume when endpoints recover." subtitle: HTTP round-trip latency for chain_getHeader against every free, no-key public Hydration Polkadot parachain endpoint, audited every 60 seconds from 3 regions. category: RPCs -status: live +status: suspended metric: RPC latency unit: ms higher_is_better: false seo_intro: | - Hydration (formerly HydraDX) is an Omnipool-based DEX parachain on Polkadot enabling single-sided liquidity provision across assets. HDX is the native token. Five keyless endpoints are probed continuously: Hydration's official gateway (rpc.hydradx.cloud), Dwellir, Helikon, Dotters Network, and Parachains Network. + Hydration (formerly HydraDX) is an Omnipool-based DEX parachain on Polkadot enabling single-sided liquidity provision across assets. HDX is the native token. This bench is currently suspended: all 5 previously measured providers (Hydration official, Dwellir, Helikon, Dotters Network, Parachains Network) are either DNS dead or returning HTTP 400. The bench will resume when endpoints recover. abstract: | Per-chain member of the RPC latency cluster, extended to Hydration. - We measure the round-trip latency of a single, identical JSON-RPC call - (`chain_getHeader`) against every no-key public Hydration parachain endpoint - that sustains continuous probing, 5 providers at launch, - every 60 seconds, from us-east, eu-west and Singapore. The cross-chain - view lives on the parent `rpc-capabilities` benchmark; this page is the - Hydration-scoped answer with per-region breakdowns as a first-class dimension. + This bench is currently suspended: all 5 previously measured providers + are DNS dead or returning HTTP 400. Data will resume when at least + one keyless Hydration RPC endpoint becomes reachable again. 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." @@ -31,16 +28,16 @@ methodology: - "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` (HTTP 200 + parsable hex block number), `http_err`, `jsonrpc_err`, `stale` (more than 40 blocks behind the cross-provider tip), `timeout`." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"hydration\"`. Provider coverage at launch: 5 no-key endpoints." + - "Chain scope: every query on this page is pinned to `chain=\"hydration\"`. Bench currently suspended: all 5 previously measured providers are DNS dead or returning HTTP 400." findings: - - "{{best_name}} currently leads free Hydration RPC at {{best_p50}} (`chain_getHeader` p50, 24h) across 5 measured providers." + - "Hydration RPC bench is suspended. All previously measured providers (Hydration official, Dwellir, Helikon, Dotters Network, Parachains Network) are unreachable." faq: - q: "What is the fastest free Hydration RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`chain_getHeader` p50 over the last 24h), measured against 5 no-key providers probed every 60 seconds from us-east, eu-west and Singapore." + a: "This bench is currently suspended. All 5 previously measured keyless providers are DNS dead or returning HTTP 400. The bench will resume when at least one reachable endpoint is found." - q: "Which Hydration RPCs work without an API key?" - a: "5 endpoints sustain continuous keyless probing: Hydration official, Dwellir, Helikon, Dotters Network, and Parachains Network. Every endpoint was live-verified before inclusion." + a: "All 5 previously verified endpoints (Hydration official, Dwellir, Helikon, Dotters Network, Parachains Network) are currently unreachable. The bench will resume when providers recover." - q: "Does the fastest Hydration RPC change by region?" a: "Often. The region tabs at the top of the page re-scope every number to a single origin." - q: "How is Hydration RPC latency measured here?" @@ -61,118 +58,7 @@ dimensions: - { value: eu-west, label: EU-West } - { value: sgp, label: Singapore } -providers: - - slug: hydration-official - name: Hydration - tag: Hydration official public RPC gateway - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to Hydration's no-key endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="hydration-official", chain="hydration"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="hydration-official", chain="hydration"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="hydration-official", chain="hydration"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="hydration-official", chain="hydration"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="hydration-official", chain="hydration"}) / sum(ocb:rpc_call:rate_24h{provider="hydration-official", chain="hydration"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="hydration-official", chain="hydration"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="hydration-official", chain="hydration"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="hydration-official", chain="hydration", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="hydration-official", chain="hydration", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="hydration-official", chain="hydration", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="hydration-official", chain="hydration", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="hydration-official", chain="hydration", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="hydration-official", chain="hydration", region="sgp"}[1h]) - - - slug: dwellir - name: Dwellir - tag: Dwellir Substrate infrastructure provider - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to Dwellir's no-key Hydration endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="hydration"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="dwellir", chain="hydration"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="dwellir", chain="hydration"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="dwellir", chain="hydration"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="dwellir", chain="hydration"}) / sum(ocb:rpc_call:rate_24h{provider="dwellir", chain="hydration"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="dwellir", chain="hydration"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="hydration"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="hydration", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="hydration", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="hydration", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="hydration", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dwellir", chain="hydration", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="dwellir", chain="hydration", region="sgp"}[1h]) - - - slug: helikon - name: Helikon - tag: Helikon community RPC for Hydration - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to Helikon's no-key Hydration endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="helikon", chain="hydration"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="helikon", chain="hydration"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="helikon", chain="hydration"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="helikon", chain="hydration"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="helikon", chain="hydration"}) / sum(ocb:rpc_call:rate_24h{provider="helikon", chain="hydration"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="helikon", chain="hydration"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="helikon", chain="hydration"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="helikon", chain="hydration", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="helikon", chain="hydration", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="helikon", chain="hydration", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="helikon", chain="hydration", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="helikon", chain="hydration", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="helikon", chain="hydration", region="sgp"}[1h]) - - - slug: dotters-network - name: Dotters Network - tag: Dotters Network community RPC for Hydration - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to Dotters Network's no-key Hydration endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dotters-network", chain="hydration"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="dotters-network", chain="hydration"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="dotters-network", chain="hydration"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="dotters-network", chain="hydration"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="dotters-network", chain="hydration"}) / sum(ocb:rpc_call:rate_24h{provider="dotters-network", chain="hydration"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="dotters-network", chain="hydration"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="dotters-network", chain="hydration"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dotters-network", chain="hydration", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="dotters-network", chain="hydration", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dotters-network", chain="hydration", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="dotters-network", chain="hydration", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="dotters-network", chain="hydration", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="dotters-network", chain="hydration", region="sgp"}[1h]) - - - slug: parachains-network - name: Parachains Network - tag: Parachains.network community RPC for Hydration - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to Parachains Network's no-key Hydration endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="parachains-network", chain="hydration"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="parachains-network", chain="hydration"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="parachains-network", chain="hydration"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="parachains-network", chain="hydration"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="parachains-network", chain="hydration"}) / sum(ocb:rpc_call:rate_24h{provider="parachains-network", chain="hydration"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="parachains-network", chain="hydration"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="parachains-network", chain="hydration"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="parachains-network", chain="hydration", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="parachains-network", chain="hydration", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="parachains-network", chain="hydration", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="parachains-network", chain="hydration", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="parachains-network", chain="hydration", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="parachains-network", chain="hydration", region="sgp"}[1h]) +providers: [] +# All 5 previously measured providers are DNS dead or returning HTTP 400. +# Suspended 2026-08-07. Will resume when endpoints recover. +# Removed: hydration-official, dwellir, helikon, dotters-network, parachains-network diff --git a/benchmarks/kaia-rpc.yml b/benchmarks/kaia-rpc.yml index 24a1c0b3..338f5432 100644 --- a/benchmarks/kaia-rpc.yml +++ b/benchmarks/kaia-rpc.yml @@ -4,7 +4,7 @@ slug: kaia-rpc number: "098" title: Fastest free Kaia RPC, live no-key EVM endpoint latency seo_title: "Fastest free Kaia RPC 2026" -seo_description: "{{best_name}} leads free Kaia RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h). 2 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Kaia RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h). 1 no-key provider measured every 60s from 3 regions." subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public Kaia EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,13 +14,13 @@ unit: ms higher_is_better: false seo_intro: | - Kaia is the EVM L1 formed by the Klaytn + Finschia merger in Aug 2024, with Istanbul BFT consensus, KAIA gas token, and dominant footprint in Korea + Japan through LINE and Kakao integrations. Standard EVM JSON-RPC. Excluded by the launch audit: BlockPI (`kaia.blockpi.network` returned `Apikey not found`), Nodies (`klaytn-pokt.nodies.app` requires paid subscription), Alchemy public (`KAIA_MAINNET not enabled for this app` on public tier), AllThatNode + OnFinality + Chainstack + Tatum + NowNodes (DNS-fail or key required). Every provider live-verified with 4 consecutive keyless probes at launch. + Kaia is the EVM L1 formed by the Klaytn + Finschia merger in Aug 2024, with Istanbul BFT consensus, KAIA gas token, and dominant footprint in Korea + Japan through LINE and Kakao integrations. Standard EVM JSON-RPC. Excluded: dRPC (500 paid plan only), BlockPI (`kaia.blockpi.network` returned `Apikey not found`), Nodies (`klaytn-pokt.nodies.app` requires paid subscription), Alchemy public (`KAIA_MAINNET not enabled for this app` on public tier), AllThatNode + OnFinality + Chainstack + Tatum + NowNodes (DNS-fail or key required). Every provider live-verified with 4 consecutive keyless probes at launch. abstract: | Per-chain member of the RPC latency cluster, extended to Kaia. We measure the round-trip latency of a single, identical JSON-RPC call - (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public Kaia endpoint - that sustains continuous probing, 2 providers at launch, + (`eth_getBlockByNumber(\"latest\", false)`) against the no-key public Kaia endpoint + that sustains continuous probing (1 provider: Kaia Foundation official), every 60 seconds, from us-east, eu-west and Singapore. The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) with a Kaia-scaled staleness gap (25 blocks at @@ -35,16 +35,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 25 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"kaia\"`. Provider coverage at launch: 2 no-key endpoints (Kaia Foundation, dRPC)." + - "Chain scope: every query on this page is pinned to `chain=\"kaia\"`. Provider coverage: 1 no-key endpoint (Kaia Foundation). dRPC removed (500 paid plan only)." findings: - - "{{best_name}} currently leads free Kaia RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h) across 2 measured providers." + - "{{best_name}} currently leads free Kaia RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h) — 1 measured provider." faq: - q: "What is the fastest free Kaia RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 2 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 1 no-key provider probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment." - q: "Which Kaia RPCs work without an API key?" - a: "2 endpoints sustain continuous keyless probing at launch: Kaia Foundation, dRPC. Every listed endpoint was live-verified with a `eth_getBlockByNumber(\"latest\", false)` POST returning a parsable response before inclusion. See the methodology section for the specific endpoints that were audited and excluded." + a: "1 endpoint sustains continuous keyless probing: Kaia Foundation (public-en.node.kaia.io). dRPC was removed after returning 500 (paid plan only). See the methodology section for the specific endpoints that were audited and excluded." - q: "Does the fastest Kaia RPC change by region?" a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate." - q: "How is Kaia RPC latency measured here, technically?" @@ -89,26 +89,4 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="kaia-official", chain="kaia", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="kaia-official", chain="kaia", region="sgp"}[1h]) - - slug: drpc - name: dRPC - tag: Multi-provider RPC load balancer, edge-close routing - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to dRPC's no-key kaia RPC endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="kaia"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="kaia"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="kaia"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="kaia"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="kaia"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="kaia"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="kaia"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="kaia"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="kaia", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="kaia", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="kaia", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="kaia", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="kaia", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="kaia", region="sgp"}[1h]) diff --git a/benchmarks/moonriver-rpc.yml b/benchmarks/moonriver-rpc.yml index a8f62b3e..e62325da 100644 --- a/benchmarks/moonriver-rpc.yml +++ b/benchmarks/moonriver-rpc.yml @@ -4,7 +4,7 @@ slug: moonriver-rpc number: "135" title: Fastest free Moonriver RPC, live no-key EVM endpoint latency seo_title: "Fastest free Moonriver RPC 2026" -seo_description: "{{best_name}} leads free Moonriver RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h). 4 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Moonriver RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h). 3 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public Moonriver EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,14 +14,14 @@ unit: ms higher_is_better: false seo_intro: | - Moonriver is the Kusama canary network for Moonbeam (chain 1285), a full EVM parachain with Ethereum-compatible tooling, MOVR gas token, ~12 s block time under Nominated Proof of Stake, serving as the staging ground for Moonbeam features before they reach Polkadot. The 2026-08-03 audit confirms 4 clean keyless providers: the Moonbeam Foundation official endpoint (rpc.api.moonriver.moonbeam.network), PublicNode (moonriver-rpc.publicnode.com), OnFinality (moonriver.api.onfinality.io/public), and UnitedBloc (moonriver.unitedbloc.com). Excluded: Ankr (requires key for Moonriver), dRPC (no moonriver route). + Moonriver is the Kusama canary network for Moonbeam (chain 1285), a full EVM parachain with Ethereum-compatible tooling, MOVR gas token, ~12 s block time under Nominated Proof of Stake, serving as the staging ground for Moonbeam features before they reach Polkadot. The 2026-08-03 audit confirms 3 clean keyless providers: the Moonbeam Foundation official endpoint (rpc.api.moonriver.moonbeam.network), PublicNode (moonriver-rpc.publicnode.com), and OnFinality (moonriver.api.onfinality.io/public). Excluded: UnitedBloc (moonriver.unitedbloc.com DNS dead), Ankr (requires key for Moonriver), dRPC (no moonriver route). abstract: | Per-chain member of the RPC latency cluster, extended to Moonriver. We measure the round-trip latency of a single, identical JSON-RPC call (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public - Moonriver endpoint that sustains continuous probing, 4 providers - measured, every 60 seconds, from us-east, eu-west and Singapore. The + Moonriver endpoint that sustains continuous probing, 3 providers, + every 60 seconds, from us-east, eu-west and Singapore. The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) with a Moonriver-scaled staleness gap (20 blocks at Moonriver's ~12 s block time). The cross-chain view @@ -35,16 +35,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"moonriver\"`. Provider coverage: 4 no-key endpoints." + - "Chain scope: every query on this page is pinned to `chain=\"moonriver\"`. Provider coverage: 3 no-key endpoints. UnitedBloc removed (DNS dead)." findings: - - "{{best_name}} currently leads free Moonriver RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 4 measured providers." + - "{{best_name}} currently leads free Moonriver RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers." faq: - q: "What is the fastest free Moonriver RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 4 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment." - q: "Which Moonriver RPCs work without an API key?" - a: "4 endpoints sustain continuous keyless probing: Moonbeam Foundation official (rpc.api.moonriver.moonbeam.network), PublicNode (moonriver-rpc.publicnode.com), OnFinality (moonriver.api.onfinality.io/public), and UnitedBloc (moonriver.unitedbloc.com). Every listed endpoint was live-verified with a `eth_getBlockByNumber` POST returning a parsable response before inclusion." + a: "3 endpoints sustain continuous keyless probing: Moonbeam Foundation official (rpc.api.moonriver.moonbeam.network), PublicNode (moonriver-rpc.publicnode.com), and OnFinality (moonriver.api.onfinality.io/public). UnitedBloc (moonriver.unitedbloc.com) was removed after its DNS went dead." - q: "Does the fastest Moonriver RPC change by region?" a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate." - q: "How is Moonriver RPC latency measured here, technically?" @@ -135,25 +135,3 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="onfinality", chain="moonriver", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="onfinality", chain="moonriver", region="sgp"}[1h]) - - slug: unitedbloc - name: UnitedBloc - tag: UnitedBloc community node operator public RPC for Moonriver - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to UnitedBloc's no-key moonriver RPC endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="unitedbloc", chain="moonriver"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="unitedbloc", chain="moonriver"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="unitedbloc", chain="moonriver"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="unitedbloc", chain="moonriver"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="unitedbloc", chain="moonriver"}) / sum(ocb:rpc_call:rate_24h{provider="unitedbloc", chain="moonriver"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="unitedbloc", chain="moonriver"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="unitedbloc", chain="moonriver"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="unitedbloc", chain="moonriver", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="unitedbloc", chain="moonriver", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="unitedbloc", chain="moonriver", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="unitedbloc", chain="moonriver", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="unitedbloc", chain="moonriver", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="unitedbloc", chain="moonriver", region="sgp"}[1h]) diff --git a/benchmarks/ronin-rpc.yml b/benchmarks/ronin-rpc.yml index 13931024..72e4edc6 100644 --- a/benchmarks/ronin-rpc.yml +++ b/benchmarks/ronin-rpc.yml @@ -4,7 +4,7 @@ slug: ronin-rpc number: "110" title: Fastest free Ronin RPC, live no-key EVM endpoint latency seo_title: "Fastest free Ronin RPC 2026" -seo_description: "{{best_name}} leads free Ronin RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h). 4 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Ronin RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h). 3 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public Ronin EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,14 +14,14 @@ unit: ms higher_is_better: false seo_intro: | - Ronin is an EVM L1 (chain 2020) operated by Sky Mavis, purpose-built for Web3 gaming and home of Axie Infinity, Pixels and a broader gaming ecosystem. RON gas token, delegated proof-of-stake validator set, 3 s block cadence. 4 clean keyless providers live-verified at launch: `ronin-official` (api.roninchain.com), dRPC, Tenderly Gateway, Thirdweb. Excluded by the launch audit: Ronin Tech secondary domain (DNS-fail), PublicNode (no `ronin` subdomain), lgns.net (DNS-fail). + Ronin is an EVM L1 (chain 2020) operated by Sky Mavis, purpose-built for Web3 gaming and home of Axie Infinity, Pixels and a broader gaming ecosystem. RON gas token, delegated proof-of-stake validator set, 3 s block cadence. 3 clean keyless providers: `ronin-official` (api.roninchain.com), Tenderly Gateway, Thirdweb. Excluded: dRPC (500 persistent internal error after 3 consecutive attempts), Ronin Tech secondary domain (DNS-fail), PublicNode (no `ronin` subdomain), lgns.net (DNS-fail). abstract: | Per-chain member of the RPC latency cluster, extended to Ronin. We measure the round-trip latency of a single, identical JSON-RPC call (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public - Ronin endpoint that sustains continuous probing, 4 providers - at launch, every 60 seconds, from us-east, eu-west and Singapore. The + Ronin endpoint that sustains continuous probing, 3 providers, + every 60 seconds, from us-east, eu-west and Singapore. The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) with a Ronin-scaled staleness gap (25 blocks at Ronin's ~3 s block time). The cross-chain view @@ -35,16 +35,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 25 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"ronin\"`. Provider coverage at launch: 4 no-key endpoints." + - "Chain scope: every query on this page is pinned to `chain=\"ronin\"`. Provider coverage: 3 no-key endpoints. dRPC removed (500 persistent internal error)." findings: - - "{{best_name}} currently leads free Ronin RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 4 measured providers." + - "{{best_name}} currently leads free Ronin RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers." faq: - q: "What is the fastest free Ronin RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 4 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment." - q: "Which Ronin RPCs work without an API key?" - a: "4 endpoints sustain continuous keyless probing at launch. Every listed endpoint was live-verified with a `eth_getBlockByNumber` POST returning a parsable response before inclusion. See the methodology section for the endpoints that were audited and excluded." + a: "3 endpoints sustain continuous keyless probing: Sky Mavis official, Tenderly Gateway, Thirdweb. dRPC was removed after persistent 500 internal errors. Every listed endpoint was live-verified with a `eth_getBlockByNumber` POST returning a parsable response before inclusion." - q: "Does the fastest Ronin RPC change by region?" a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate." - q: "How is Ronin RPC latency measured here, technically?" @@ -89,29 +89,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ronin-official", chain="ronin", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="ronin-official", chain="ronin", region="sgp"}[1h]) - - slug: drpc - name: dRPC - tag: dRPC public gateway routing to Ronin node pool - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to dRPC's no-key ronin RPC endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="ronin"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="ronin"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="ronin"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="ronin"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="ronin"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="ronin"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="ronin"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="ronin"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="ronin", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="ronin", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="ronin", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="ronin", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="ronin", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="ronin", region="sgp"}[1h]) - - slug: tenderly name: Tenderly Gateway tag: Tenderly Gateway public RPC for Ronin diff --git a/benchmarks/sei-rpc.yml b/benchmarks/sei-rpc.yml index 933eaf48..97776621 100644 --- a/benchmarks/sei-rpc.yml +++ b/benchmarks/sei-rpc.yml @@ -4,7 +4,7 @@ slug: sei-rpc number: "108" title: Fastest free Sei RPC, live no-key EVM endpoint latency seo_title: "Fastest free Sei RPC 2026" -seo_description: "{{best_name}} leads free Sei RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h). 4 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Sei RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h). 3 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for the `eth_getBlockByNumber(\"latest\", false)` method against every free, no-key public Sei EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,14 +14,14 @@ unit: ms higher_is_better: false seo_intro: | - Sei is a Cosmos SDK L1 (chain 1329) with a parallel-execution EVM layer bolted onto Tendermint consensus, launched by Sei Labs and positioned as a high-throughput trading chain. Standard EVM JSON-RPC on the EVM side; the Cosmos JSON-RPC would need a separate `Kind:"cosmos"` cluster entry as we did for Osmosis. Excluded by the launch audit: PublicNode (no `sei-evm` subdomain), Tenderly public (no `sei` route), BlockPI (`Apikey not found`), MeowRPC (defunct), AllThatNode + Basement Nodes + Node75 (host unresolvable or key-gated). The 2026-07-03 sweep had originally excluded Sei because dRPC cached `eth_blockNumber`; the re-audit on 2026-07-26 confirms dRPC no longer caches and Thirdweb + Stakeme have come online, restoring 4 clean keyless providers. Every listed provider live-verified with 4 consecutive keyless probes at re-launch. + Sei is a Cosmos SDK L1 (chain 1329) with a parallel-execution EVM layer bolted onto Tendermint consensus, launched by Sei Labs and positioned as a high-throughput trading chain. Standard EVM JSON-RPC on the EVM side; the Cosmos JSON-RPC would need a separate `Kind:"cosmos"` cluster entry as we did for Osmosis. 3 clean keyless providers measured: sei-official, Thirdweb, Stakeme. Excluded: dRPC (500 persistent internal error after 3 consecutive attempts), PublicNode (no `sei-evm` subdomain), Tenderly public (no `sei` route), BlockPI (`Apikey not found`), MeowRPC (defunct), AllThatNode + Basement Nodes + Node75 (host unresolvable or key-gated). abstract: | Per-chain member of the RPC latency cluster, extended to Sei. We measure the round-trip latency of a single, identical JSON-RPC call (`eth_getBlockByNumber(\"latest\", false)`) against every no-key public - Sei endpoint that sustains continuous probing, 4 providers - at launch, every 60 seconds, from us-east, eu-west and Singapore. The + Sei endpoint that sustains continuous probing, 3 providers, + every 60 seconds, from us-east, eu-west and Singapore. The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) with a Sei-scaled staleness gap (25 blocks at Sei's ~1 s block time). The cross-chain view @@ -35,16 +35,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms → 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 25 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"sei\"`. Provider coverage at launch: 4 no-key endpoints." + - "Chain scope: every query on this page is pinned to `chain=\"sei\"`. Provider coverage: 3 no-key endpoints. dRPC removed (500 persistent internal error)." findings: - - "{{best_name}} currently leads free Sei RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 4 measured providers." + - "{{best_name}} currently leads free Sei RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers." faq: - q: "What is the fastest free Sei RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 4 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples. Use the region tabs to see the leader from the origin closest to your deployment." - q: "Which Sei RPCs work without an API key?" - a: "4 endpoints sustain continuous keyless probing at launch. Every listed endpoint was live-verified with a `eth_getBlockByNumber` POST returning a parsable response before inclusion. See the methodology section for the endpoints that were audited and excluded." + a: "3 endpoints sustain continuous keyless probing: Sei Labs official, Thirdweb, and Stakeme. dRPC was removed after persistent 500 internal errors. Every listed endpoint was live-verified with a `eth_getBlockByNumber` POST returning a parsable response before inclusion." - q: "Does the fastest Sei RPC change by region?" a: "Often. Public infra concentrates in specific regions; a gateway that wins from Amsterdam can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate." - q: "How is Sei RPC latency measured here, technically?" @@ -89,29 +89,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="sei-official", chain="sei", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="sei-official", chain="sei", region="sgp"}[1h]) - - slug: drpc - name: dRPC - tag: dRPC gateway routing to Sei EVM node pool - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to dRPC's no-key sei RPC endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="sei"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="sei"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="sei"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="sei"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="sei"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="sei"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="sei"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="sei"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="sei", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="sei", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="sei", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="sei", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="sei", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="sei", region="sgp"}[1h]) - - slug: thirdweb name: Thirdweb tag: Thirdweb public RPC gateway for Sei EVM diff --git a/benchmarks/songbird-rpc.yml b/benchmarks/songbird-rpc.yml index d8207bac..cf80f9c5 100644 --- a/benchmarks/songbird-rpc.yml +++ b/benchmarks/songbird-rpc.yml @@ -4,7 +4,7 @@ slug: songbird-rpc number: "192" title: Fastest free Songbird RPC, live no-key endpoint latency seo_title: "Fastest free Songbird RPC 2026" -seo_description: "{{best_name}} leads free Songbird RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 4 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Songbird RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 1 no-key provider measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public Songbird EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,13 +14,13 @@ unit: ms higher_is_better: false seo_intro: | - Songbird (chain 19, SGB) is Flare Network's canary network, a production EVM-compatible chain used to test and validate Flare's FTSO oracle system before mainnet deployment. The Flare Foundation operates four keyless endpoints: the official Songbird gateway (songbird-api.flare.network/ext/C/rpc), dRPC (songbird.drpc.org), Blast API, and the community node sgb.ftso.com.au. + Songbird (chain 19, SGB) is Flare Network's canary network, a production EVM-compatible chain used to test and validate Flare's FTSO oracle system before mainnet deployment. One keyless endpoint is probed continuously: the official Songbird gateway (songbird-api.flare.network/ext/C/rpc). The FTSO AU community node (songbird.rpc.ftso.au) was removed after DNS went dead. abstract: | Per-chain member of the RPC latency cluster, extended to Songbird. We measure the round-trip latency of a single, identical RPC call - (`eth_getBlockByNumber`) against every no-key public Songbird endpoint - that sustains continuous probing, 4 providers at launch, + (`eth_getBlockByNumber`) against the no-key public Songbird endpoint + that sustains continuous probing (1 provider: Flare Foundation official), every 60 seconds, from us-east, eu-west and Singapore. The cross-chain view lives on the parent `rpc-capabilities` benchmark; this page is the Songbird-scoped answer with per-region breakdowns as a first-class dimension. @@ -31,16 +31,16 @@ methodology: - "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` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err`, `stale` (more than 20 blocks behind the cross-provider tip), `timeout`." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"songbird\"`. Provider coverage at launch: 4 no-key endpoints." + - "Chain scope: every query on this page is pinned to `chain=\"songbird\"`. Provider coverage: 1 no-key endpoint (Flare Foundation official). FTSO AU removed after DNS went dead." findings: - - "{{best_name}} currently leads free Songbird RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 4 measured providers." + - "{{best_name}} currently leads free Songbird RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) — 1 measured provider." faq: - q: "What is the fastest free Songbird RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 4 no-key providers probed every 60 seconds from us-east, eu-west and Singapore." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 1 no-key provider probed every 60 seconds from us-east, eu-west and Singapore." - q: "Which Songbird RPCs work without an API key?" - a: "4 endpoints sustain continuous keyless probing: Flare Foundation official, dRPC, Blast API, and ftso.com.au community node. Every endpoint was live-verified before inclusion." + a: "1 endpoint sustains continuous keyless probing: Flare Foundation official (songbird-api.flare.network/ext/C/rpc). The FTSO AU community node was removed after its DNS went dead." - q: "Does the fastest Songbird RPC change by region?" a: "Often. The region tabs at the top of the page re-scope every number to a single origin." - q: "How is Songbird RPC latency measured here?" @@ -85,48 +85,3 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="flare-official", chain="songbird", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="flare-official", chain="songbird", region="sgp"}[1h]) - - slug: drpc - name: dRPC - tag: dRPC public gateway for Songbird canary network - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to dRPC's no-key Songbird endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="songbird"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="songbird"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="songbird"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="songbird"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="songbird"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="songbird"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="songbird"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="songbird"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="songbird", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="songbird", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="songbird", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="songbird", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="songbird", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="songbird", region="sgp"}[1h]) - - - slug: ftso-au - name: FTSO AU - tag: ftso.com.au community Songbird RPC node - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to ftso.com.au's no-key Songbird endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ftso-au", chain="songbird"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="ftso-au", chain="songbird"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="ftso-au", chain="songbird"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="ftso-au", chain="songbird"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="ftso-au", chain="songbird"}) / sum(ocb:rpc_call:rate_24h{provider="ftso-au", chain="songbird"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="ftso-au", chain="songbird"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="ftso-au", chain="songbird"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ftso-au", chain="songbird", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ftso-au", chain="songbird", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ftso-au", chain="songbird", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ftso-au", chain="songbird", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ftso-au", chain="songbird", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="ftso-au", chain="songbird", region="sgp"}[1h]) diff --git a/benchmarks/telos-rpc.yml b/benchmarks/telos-rpc.yml index 00c3d939..78885aee 100644 --- a/benchmarks/telos-rpc.yml +++ b/benchmarks/telos-rpc.yml @@ -4,7 +4,7 @@ slug: telos-rpc number: "195" title: Fastest free Telos EVM RPC, live no-key endpoint latency seo_title: "Fastest free Telos EVM RPC 2026" -seo_description: "{{best_name}} leads free Telos EVM RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 3 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Telos EVM RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 1 no-key provider measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public Telos EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,13 +14,13 @@ unit: ms higher_is_better: false seo_intro: | - Telos EVM (chain 40) is a high-performance EVM-compatible blockchain with sub-0.5 second block times, TLOS as the gas token, and a carbon-neutral operational commitment. One of the longest-running EVM-compatible chains, live since 2021. Three keyless endpoints are probed continuously: Telos Foundation official (mainnet.telos.net/evm), dRPC (telos.drpc.org), and Blast API. + Telos EVM (chain 40) is a high-performance EVM-compatible blockchain with sub-0.5 second block times, TLOS as the gas token, and a carbon-neutral operational commitment. One of the longest-running EVM-compatible chains, live since 2021. One keyless endpoint is probed continuously: dRPC (telos.drpc.org). The Telos Foundation official endpoint returned 404 and has been removed. abstract: | Per-chain member of the RPC latency cluster, extended to Telos EVM. We measure the round-trip latency of a single, identical RPC call - (`eth_getBlockByNumber`) against every no-key public Telos EVM endpoint - that sustains continuous probing, 3 providers at launch, + (`eth_getBlockByNumber`) against the no-key public Telos EVM endpoint + that sustains continuous probing (1 provider: dRPC), every 60 seconds, from us-east, eu-west and Singapore. The cross-chain view lives on the parent `rpc-capabilities` benchmark; this page is the Telos-scoped answer with per-region breakdowns as a first-class dimension. @@ -31,16 +31,16 @@ methodology: - "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` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err`, `stale` (more than 20 blocks behind the cross-provider tip), `timeout`." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"telos\"`. Provider coverage at launch: 3 no-key endpoints." + - "Chain scope: every query on this page is pinned to `chain=\"telos\"`. Provider coverage: 1 no-key endpoint (dRPC). Telos Foundation official returned 404 and has been removed." findings: - - "{{best_name}} currently leads free Telos EVM RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers." + - "{{best_name}} currently leads free Telos EVM RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) — 1 measured provider." faq: - q: "What is the fastest free Telos EVM RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 1 no-key provider probed every 60 seconds from us-east, eu-west and Singapore." - q: "Which Telos EVM RPCs work without an API key?" - a: "3 endpoints sustain continuous keyless probing: Telos Foundation official, dRPC, and Blast API. Every endpoint was live-verified before inclusion." + a: "1 endpoint sustains continuous keyless probing: dRPC (telos.drpc.org). The Telos Foundation official endpoint (mainnet.telos.net/evm) returned 404 and has been removed." - q: "Does the fastest Telos RPC change by region?" a: "Often. The region tabs at the top of the page re-scope every number to a single origin." - q: "How is Telos EVM RPC latency measured here?" @@ -62,29 +62,6 @@ dimensions: - { value: sgp, label: Singapore } providers: - - slug: telos-official - name: Telos Foundation - tag: Telos Foundation official public EVM RPC - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to Telos Foundation's no-key endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="telos-official", chain="telos"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="telos-official", chain="telos"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="telos-official", chain="telos"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="telos-official", chain="telos"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="telos-official", chain="telos"}) / sum(ocb:rpc_call:rate_24h{provider="telos-official", chain="telos"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="telos-official", chain="telos"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="telos-official", chain="telos"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="telos-official", chain="telos", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="telos-official", chain="telos", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="telos-official", chain="telos", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="telos-official", chain="telos", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="telos-official", chain="telos", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="telos-official", chain="telos", region="sgp"}[1h]) - - slug: drpc name: dRPC tag: dRPC public gateway for Telos EVM diff --git a/benchmarks/tron-rpc.yml b/benchmarks/tron-rpc.yml index 627b2521..98ff4587 100644 --- a/benchmarks/tron-rpc.yml +++ b/benchmarks/tron-rpc.yml @@ -4,7 +4,7 @@ slug: tron-rpc number: "093" title: Fastest free TRON RPC, live no-key JSON-RPC endpoint latency seo_title: "Fastest free TRON RPC 2026" -seo_description: "{{best_name}} leads free TRON RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 3 no-key JSON-RPC-compat providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free TRON RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 2 no-key JSON-RPC-compat providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for `eth_getBlockByNumber("latest", false)` against every free, no-key public TRON JSON-RPC-compatible endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -20,8 +20,9 @@ seo_intro: | Ethereum method names (`eth_blockNumber`, `eth_getBlockByNumber`, `eth_getBalance`). This bench measures the JSON-RPC surface because that is the path every TRON-compatible EVM wallet, TronWeb bridge - and cross-chain bot integrates against. Provider cohort at launch: - TronGrid (Tron Foundation official), dRPC and PublicNode / Allnodes. + and cross-chain bot integrates against. Provider cohort: + TronGrid (Tron Foundation official) and PublicNode / Allnodes. + dRPC was removed after the TRON JSON-RPC method became unavailable. Ankr's JSON-RPC path is keyless in branding but rejected our probes with `API key is not allowed to access blockchain`; Chainstack, NOWNodes, GetBlock, Tatum and BlockPI require API keys on the TRON @@ -36,7 +37,7 @@ abstract: | single, identical JSON-RPC call (`eth_getBlockByNumber("latest", false)`) against every no-key public TRON JSON-RPC endpoint that sustains continuous probing, - 3 providers at launch, every 60 seconds, from us-east, eu-west and + 2 providers, every 60 seconds, from us-east, eu-west and Singapore. The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) with the EVM staleness gap tuned for TRON's ~3 s block time (25 blocks ≈ 75 @@ -51,22 +52,21 @@ methodology: - "Call-result classification: `ok` (HTTP 200 + non-empty result with a parsable hex block number), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 25 blocks behind the cross-provider tip, roughly 75 seconds at TRON's ~3 s block time), `timeout`. Latency without reliability is a misleading ranking signal." - "Surface scope: only the EVM-compatible JSON-RPC surface (`/jsonrpc` endpoints) is measured, because that is the path every cross-chain wallet, TronWeb / EVM bridge and arbitrage bot integrates against. The native TRON REST API (`wallet/getnowblock`, `wallet/triggersmartcontract`) is out of scope for this bench; a chain-specific `tron-rest` bench can be added later when there is clear demand from integrators using TronWeb directly." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"tron\"`. Provider coverage at launch: 3 no-key JSON-RPC endpoints (TronGrid, dRPC, PublicNode). The TRON JSON-RPC provider market is materially smaller than the EVM one; most TRON infra vendors expose only the native TRON REST API keyless, and gate the JSON-RPC path behind an API key." - - "Excluded by the launch audit: Ankr (`rpc.ankr.com/tron_jsonrpc` returned `API key is not allowed to access blockchain` without a key), Chainstack / NOWNodes / GetBlock / Tatum / BlockPI (all require an API key on the TRON JSON-RPC path even though branded as public), OnFinality + Blast API + AllThatNode (no public TRON JSON-RPC gateway), Tron Foundation's `rpc.trongrid.io` (DNS not resolving; the correct official host is `api.trongrid.io/jsonrpc`)." + - "Chain scope: every query on this page is pinned to `chain=\"tron\"`. Provider coverage: 2 no-key JSON-RPC endpoints (TronGrid, PublicNode). dRPC removed (method not available). The TRON JSON-RPC provider market is materially smaller than the EVM one; most TRON infra vendors expose only the native TRON REST API keyless, and gate the JSON-RPC path behind an API key." + - "Excluded by the launch audit: dRPC (method not available on TRON JSON-RPC path), Ankr (`rpc.ankr.com/tron_jsonrpc` returned `API key is not allowed to access blockchain` without a key), Chainstack / NOWNodes / GetBlock / Tatum / BlockPI (all require an API key on the TRON JSON-RPC path even though branded as public), OnFinality + Blast API + AllThatNode (no public TRON JSON-RPC gateway), Tron Foundation's `rpc.trongrid.io` (DNS not resolving; the correct official host is `api.trongrid.io/jsonrpc`)." findings: - - "{{best_name}} currently leads free TRON RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h) across 3 measured providers." + - "{{best_name}} currently leads free TRON RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h) across 2 measured providers." - "{{name:trongrid}} is Tron Foundation's official public API gateway; the bench measures the same `api.trongrid.io/jsonrpc` endpoint that every TronWeb + tronbox integrator points at first. Latency here reflects what a keyless TRON-EVM integrator sees end-to-end." - - "{{name:drpc}} routes TRON JSON-RPC through its multi-provider load balancer; latency here is a good proxy for what a cross-chain integrator (multi-chain wallets, bridges) can expect from a load-balanced setup." - "{{name:publicnode}} extends its universal multi-chain footprint to TRON JSON-RPC without a keyed path; the reliability column matters as much as p50 since TRON JSON-RPC compat is a thin layer over the native TRON API and can drift under load." faq: - q: "What is the fastest free TRON RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (eth_getBlockByNumber p50 over the last 24h), measured against 3 no-key JSON-RPC providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." + a: "{{best_name}} currently leads at {{best_p50}} (eth_getBlockByNumber p50 over the last 24h), measured against 2 no-key JSON-RPC providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." - q: "Which TRON RPCs work without an API key?" - a: "Three JSON-RPC endpoints sustain continuous keyless probing at launch: TronGrid (`api.trongrid.io/jsonrpc`), dRPC (`tron.drpc.org`) and PublicNode / Allnodes (`tron.publicnode.com/jsonrpc`). Every listed endpoint was live-verified with an `eth_getBlockByNumber` POST returning a parsable hex head before inclusion. Excluded by the audit: Ankr's TRON JSON-RPC path rejects keyless probes with `API key is not allowed to access blockchain`; Chainstack, NOWNodes, GetBlock, Tatum and BlockPI require an API key on the TRON JSON-RPC path even though branded as public; OnFinality, Blast API and AllThatNode have no public TRON JSON-RPC gateway. The TRON JSON-RPC provider market is materially smaller than the EVM one , most TRON infra vendors expose only the native TRON REST API keyless." - - q: "Why only 3 providers when other RPC benches have 5+?" - a: "TRON's JSON-RPC compat layer is thinner than the EVM one, and most infra vendors gate it behind an API key even when their native TRON REST endpoints are keyless. The 3 providers on this page are all keyless endpoints that responded correctly to a plain POST during the launch audit; when other vendors open a keyless JSON-RPC path we will add them. Latency and reliability numbers here are still meaningful because the 3 providers span a Foundation-official endpoint, a multi-provider aggregator (dRPC) and a universal multi-chain gateway (PublicNode) , the three infra archetypes an integrator would compare against each other anyway." + a: "Two JSON-RPC endpoints sustain continuous keyless probing: TronGrid (`api.trongrid.io/jsonrpc`) and PublicNode / Allnodes (`tron.publicnode.com/jsonrpc`). dRPC was removed after the TRON JSON-RPC method became unavailable. Ankr's TRON JSON-RPC path rejects keyless probes with `API key is not allowed to access blockchain`; Chainstack, NOWNodes, GetBlock, Tatum and BlockPI require an API key on the TRON JSON-RPC path even though branded as public; OnFinality, Blast API and AllThatNode have no public TRON JSON-RPC gateway." + - q: "Why only 2 providers when other RPC benches have 5+?" + a: "TRON's JSON-RPC compat layer is thinner than the EVM one, and most infra vendors gate it behind an API key even when their native TRON REST endpoints are keyless. dRPC was removed after the TRON JSON-RPC method became unavailable. The 2 providers on this page are all keyless endpoints that responded correctly to a plain POST; when other vendors open a keyless JSON-RPC path we will add them." - q: "Does this bench cover the native TRON REST API (`wallet/getnowblock`)?" a: "No. This bench scopes only the EVM-compatible JSON-RPC surface (`/jsonrpc` endpoints), because that is the path cross-chain wallets, TronWeb bridges, and arbitrage bots integrate against. The native TRON REST API surface (used by TronWeb + TronBox directly) has different method semantics (address encoding, resource model, `wallet/*` vs `walletsolidity/*` paths) and would need a chain-specific probe added. That will land as a separate `tron-rest` bench when the demand from integrators using TronWeb directly is clear." - q: "Does the fastest TRON RPC change by region?" @@ -116,29 +116,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="trongrid", chain="tron", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="trongrid", chain="tron", region="sgp"}[1h]) - - slug: drpc - name: dRPC - tag: Multi-provider RPC load balancer, edge-close routing - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber(\"latest\", false)` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to dRPC's no-key TRON JSON-RPC endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="tron"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="tron"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="tron"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="tron"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="tron"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="tron"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="tron"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="tron"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="tron", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="tron", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="tron", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="tron", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="tron", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="tron", region="sgp"}[1h]) - - slug: publicnode name: PublicNode tag: Allnodes-operated, 70+ chains, keyless TRON JSON-RPC path diff --git a/benchmarks/warden-rpc.yml b/benchmarks/warden-rpc.yml index 4aa2f76c..c342e0ac 100644 --- a/benchmarks/warden-rpc.yml +++ b/benchmarks/warden-rpc.yml @@ -4,7 +4,7 @@ slug: warden-rpc number: "197" title: Fastest free Warden Protocol RPC, live no-key endpoint latency seo_title: "Fastest free Warden Protocol RPC 2026" -seo_description: "{{best_name}} leads free Warden Protocol RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 3 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Warden Protocol RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 2 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public Warden Protocol EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,13 +14,13 @@ unit: ms higher_is_better: false seo_intro: | - Warden Protocol is a modular intent-based L1 for cross-chain smart contract security, built by the Qredo team with WARD as the native gas token. EVM-compatible with Cosmos SDK internals, enabling omnichain key management and intent execution via SpaceWard. Three keyless EVM endpoints are probed continuously: PublicNode (warden-evm-rpc.publicnode.com), dRPC (warden.drpc.org), and the Warden Foundation official endpoint (evm.wardenprotocol.org). + Warden Protocol is a modular intent-based L1 for cross-chain smart contract security, built by the Qredo team with WARD as the native gas token. EVM-compatible with Cosmos SDK internals, enabling omnichain key management and intent execution via SpaceWard. Two keyless EVM endpoints are probed continuously: PublicNode (warden-evm-rpc.publicnode.com) and the Warden Foundation official endpoint (evm.wardenprotocol.org). abstract: | Per-chain member of the RPC latency cluster, extended to Warden Protocol. We measure the round-trip latency of a single, identical RPC call (`eth_getBlockByNumber`) against every no-key public Warden EVM endpoint - that sustains continuous probing, 3 providers at launch, + that sustains continuous probing, 2 providers at launch, every 60 seconds, from us-east, eu-west and Singapore. The cross-chain view lives on the parent `rpc-capabilities` benchmark; this page is the Warden-scoped answer with per-region breakdowns as a first-class dimension. @@ -31,16 +31,16 @@ methodology: - "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` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err`, `stale` (more than 20 blocks behind the cross-provider tip), `timeout`." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"warden\"`. Provider coverage at launch: 3 no-key endpoints." + - "Chain scope: every query on this page is pinned to `chain=\"warden\"`. Provider coverage at launch: 2 no-key endpoints." findings: - - "{{best_name}} currently leads free Warden Protocol RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers." + - "{{best_name}} currently leads free Warden Protocol RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 2 measured providers." faq: - q: "What is the fastest free Warden Protocol RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 2 no-key providers probed every 60 seconds from us-east, eu-west and Singapore." - q: "Which Warden Protocol RPCs work without an API key?" - a: "3 endpoints sustain continuous keyless probing: PublicNode, dRPC, and the Warden Foundation official endpoint. Every endpoint was live-verified before inclusion." + a: "2 endpoints sustain continuous keyless probing: PublicNode and the Warden Foundation official endpoint. Every endpoint was live-verified before inclusion." - q: "Does the fastest Warden RPC change by region?" a: "Often. The region tabs at the top of the page re-scope every number to a single origin." - q: "How is Warden Protocol RPC latency measured here?" diff --git a/benchmarks/zetachain-rpc.yml b/benchmarks/zetachain-rpc.yml index c53c320d..5c19737d 100644 --- a/benchmarks/zetachain-rpc.yml +++ b/benchmarks/zetachain-rpc.yml @@ -4,7 +4,7 @@ slug: zetachain-rpc number: "187" title: Fastest free ZetaChain RPC, live no-key endpoint latency seo_title: "Fastest free ZetaChain RPC 2026" -seo_description: "{{best_name}} leads free ZetaChain RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 5 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free ZetaChain RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 1 no-key provider measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public ZetaChain EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,13 +14,13 @@ unit: ms higher_is_better: false seo_intro: | - ZetaChain (chain 7000) is an EVM-compatible omnichain L1 enabling native cross-chain assets (BTC, ETH, BNB) without bridges or wrapped tokens. ZETA is the gas token. Five keyless endpoints are probed continuously: dRPC, BlockPI, Thirdweb (7000.rpc.thirdweb.com), All That Node, and Blast API. + ZetaChain (chain 7000) is an EVM-compatible omnichain L1 enabling native cross-chain assets (BTC, ETH, BNB) without bridges or wrapped tokens. ZETA is the gas token. One keyless endpoint is probed continuously: Thirdweb (7000.rpc.thirdweb.com). BlockPI returned 404 and All That Node's DNS is dead. abstract: | Per-chain member of the RPC latency cluster, extended to ZetaChain. We measure the round-trip latency of a single, identical RPC call - (`eth_getBlockByNumber`) against every no-key public ZetaChain endpoint - that sustains continuous probing, 5 providers at launch, + (`eth_getBlockByNumber`) against the no-key public ZetaChain endpoint + that sustains continuous probing (1 provider: Thirdweb), every 60 seconds, from us-east, eu-west and Singapore. The cross-chain view lives on the parent `rpc-capabilities` benchmark; this page is the ZetaChain-scoped answer with per-region breakdowns as a first-class dimension. @@ -31,16 +31,16 @@ methodology: - "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` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err`, `stale` (more than 20 blocks behind the cross-provider tip), `timeout`." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"zetachain\"`. Provider coverage at launch: 5 no-key endpoints." + - "Chain scope: every query on this page is pinned to `chain=\"zetachain\"`. Provider coverage: 1 no-key endpoint (Thirdweb). BlockPI returned 404; AllThatNode DNS is dead." findings: - - "{{best_name}} currently leads free ZetaChain RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 5 measured providers." + - "{{best_name}} currently leads free ZetaChain RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) — 1 measured provider." faq: - q: "What is the fastest free ZetaChain RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 5 no-key providers probed every 60 seconds from us-east, eu-west and Singapore." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 1 no-key provider probed every 60 seconds from us-east, eu-west and Singapore." - q: "Which ZetaChain RPCs work without an API key?" - a: "5 endpoints sustain continuous keyless probing: dRPC, BlockPI, Thirdweb, All That Node, and Blast API. Every endpoint was live-verified before inclusion." + a: "1 endpoint sustains continuous keyless probing: Thirdweb (7000.rpc.thirdweb.com). BlockPI returned 404 during the audit; AllThatNode's DNS is dead." - q: "Does the fastest ZetaChain RPC change by region?" a: "Often. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate." - q: "How is ZetaChain RPC latency measured here?" @@ -62,52 +62,6 @@ dimensions: - { value: sgp, label: Singapore } providers: - - slug: drpc - name: dRPC - tag: dRPC public gateway for ZetaChain mainnet - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to dRPC's no-key ZetaChain endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="zetachain"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="drpc", chain="zetachain"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="drpc", chain="zetachain"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="drpc", chain="zetachain"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="drpc", chain="zetachain"}) / sum(ocb:rpc_call:rate_24h{provider="drpc", chain="zetachain"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="drpc", chain="zetachain"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="zetachain"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="zetachain", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="zetachain", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="zetachain", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="zetachain", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="zetachain", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="zetachain", region="sgp"}[1h]) - - - slug: blockpi - name: BlockPI - tag: BlockPI public RPC gateway for ZetaChain - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to BlockPI's no-key ZetaChain endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blockpi", chain="zetachain"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="blockpi", chain="zetachain"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="blockpi", chain="zetachain"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="blockpi", chain="zetachain"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="blockpi", chain="zetachain"}) / sum(ocb:rpc_call:rate_24h{provider="blockpi", chain="zetachain"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="blockpi", chain="zetachain"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="blockpi", chain="zetachain"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blockpi", chain="zetachain", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blockpi", chain="zetachain", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blockpi", chain="zetachain", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blockpi", chain="zetachain", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="blockpi", chain="zetachain", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="blockpi", chain="zetachain", region="sgp"}[1h]) - - slug: thirdweb name: Thirdweb tag: Thirdweb chain-ID RPC gateway for ZetaChain @@ -131,25 +85,3 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="thirdweb", chain="zetachain", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="thirdweb", chain="zetachain", region="sgp"}[1h]) - - slug: allthatnode - name: All That Node - tag: All That Node archive-grade public gateway for ZetaChain - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to All That Node's no-key ZetaChain endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="allthatnode", chain="zetachain"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="allthatnode", chain="zetachain"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="allthatnode", chain="zetachain"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="allthatnode", chain="zetachain"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="allthatnode", chain="zetachain"}) / sum(ocb:rpc_call:rate_24h{provider="allthatnode", chain="zetachain"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="allthatnode", chain="zetachain"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="allthatnode", chain="zetachain"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="allthatnode", chain="zetachain", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="allthatnode", chain="zetachain", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="allthatnode", chain="zetachain", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="allthatnode", chain="zetachain", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="allthatnode", chain="zetachain", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="allthatnode", chain="zetachain", region="sgp"}[1h]) diff --git a/harnesses/rpc-capabilities/cmd/script/config.go b/harnesses/rpc-capabilities/cmd/script/config.go index 325b2daf..8075eccd 100644 --- a/harnesses/rpc-capabilities/cmd/script/config.go +++ b/harnesses/rpc-capabilities/cmd/script/config.go @@ -224,7 +224,7 @@ func chains() []Chain { {Slug: "tenderly", Name: "Tenderly Gateway", URL: envDefault("RPC_URL_MEGAETH_TENDERLY", "https://megaeth.gateway.tenderly.co")}, }, }, - // ─── Ethereum mainnet (9 providers) ──────────────────────── + // ─── Ethereum mainnet (8 providers) ──────────────────────── { Slug: "ethereum", Name: "Ethereum", @@ -233,12 +233,9 @@ func chains() []Chain { {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_ETHEREUM_DRPC", "https://eth.drpc.org")}, {Slug: "meowrpc", Name: "MeowRPC", URL: envDefault("RPC_URL_ETHEREUM_MEOWRPC", "https://eth.meowrpc.com")}, {Slug: "flashbots", Name: "Flashbots Protect", URL: envDefault("RPC_URL_ETHEREUM_FLASHBOTS", "https://rpc.flashbots.net")}, - {Slug: "cloudflare", Name: "Cloudflare", URL: envDefault("RPC_URL_ETHEREUM_CLOUDFLARE", "https://cloudflare-eth.com")}, {Slug: "tenderly", Name: "Tenderly Gateway", URL: envDefault("RPC_URL_ETHEREUM_TENDERLY", "https://gateway.tenderly.co/public/mainnet")}, {Slug: "nodies", Name: "Nodies (POKT)", URL: envDefault("RPC_URL_ETHEREUM_NODIES", "https://eth-pokt.nodies.app")}, {Slug: "lava", Name: "Lava Network", URL: envDefault("RPC_URL_ETHEREUM_LAVA", "https://eth1.lava.build")}, - {Slug: "blastapi", Name: "Blast API", URL: envDefault("RPC_URL_ETHEREUM_BLASTAPI", "https://eth-mainnet.public.blastapi.io")}, - {Slug: "gatewayfm", Name: "Gateway.fm", URL: envDefault("RPC_URL_ETHEREUM_GATEWAYFM", "https://rpc.eth.gateway.fm")}, {Slug: "bloxroute", Name: "bloXroute", URL: envDefault("RPC_URL_ETHEREUM_BLOXROUTE", "https://eth.rpc.blxrbdn.com")}, }, }, @@ -516,22 +513,21 @@ func chains() []Chain { // wallet + TronWeb/EVM bridge integrates against. Providers // live-verified keyless via eth_blockNumber during launch // audit: TronGrid (api.trongrid.io/jsonrpc — Tron Foundation - // official), dRPC and PublicNode/Allnodes. Excluded by that - // sweep: Ankr (API key required despite public branding), - // Chainstack + NOWNodes + GetBlock + Tatum + BlockPI (all - // require API key on TRON JSON-RPC path), OnFinality + Blast - // API + AllThatNode (no public TRON JSON-RPC gateway). The - // TRON JSON-RPC provider market is materially smaller than - // EVM — most TRON infra vendors expose only the native TRON - // REST API keyless. Native REST API surface is out of scope - // for this cluster; a `tron-rest` bench would need a + // official), PublicNode/Allnodes. Excluded: dRPC (method not + // available on TRON JSON-RPC path), Ankr (API key required + // despite public branding), Chainstack + NOWNodes + GetBlock + + // Tatum + BlockPI (all require API key on TRON JSON-RPC path), + // OnFinality + Blast API + AllThatNode (no public TRON JSON-RPC + // gateway). The TRON JSON-RPC provider market is materially + // smaller than EVM — most TRON infra vendors expose only the + // native TRON REST API keyless. Native REST API surface is out + // of scope for this cluster; a `tron-rest` bench would need a // chain-specific probe. { Slug: "tron", Name: "TRON", Providers: []Provider{ {Slug: "trongrid", Name: "TronGrid", URL: envDefault("RPC_URL_TRON_TRONGRID", "https://api.trongrid.io/jsonrpc")}, - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_TRON_DRPC", "https://tron.drpc.org")}, {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_TRON_PUBLICNODE", "https://tron.publicnode.com/jsonrpc")}, }, }, @@ -552,9 +548,9 @@ func chains() []Chain { }, // ─── Kaia (bench 098) — added 2026-07-24. EVM L1 formed by // Klaytn + Finschia merger. Providers live-verified keyless: - // kaia-official (public-en.node.kaia.io), drpc, thirdweb. - // Excluded: BlockPI (`Apikey not found`), Nodies (paid tier), - // Alchemy public (KAIA_MAINNET not enabled), AllThatNode + + // kaia-official (public-en.node.kaia.io). Excluded: dRPC (500 + // paid plan only), BlockPI (`Apikey not found`), Nodies (paid + // tier), Alchemy public (KAIA_MAINNET not enabled), AllThatNode + // OnFinality + Chainstack + Tatum + NowNodes (DNS-fail or key // required). { @@ -562,7 +558,6 @@ func chains() []Chain { Name: "Kaia", Providers: []Provider{ {Slug: "kaia-official", Name: "Kaia Foundation", URL: envDefault("RPC_URL_KAIA_OFFICIAL", "https://public-en.node.kaia.io")}, - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_KAIA_DRPC", "https://klaytn.drpc.org")}, }, }, // ─── Ink (bench 099) — added 2026-07-24. OP Stack rollup @@ -599,24 +594,18 @@ func chains() []Chain { }, // ─── Sei EVM (bench 108) — added 2026-07-26. Cosmos SDK L1 with // EVM parallel-execution layer (chain 1329). Native EVM JSON-RPC. - // The 2026-07-03 sweep excluded Sei because drpc cached - // eth_blockNumber leaving only 2 clean providers; re-audit on - // 2026-07-26 confirms dRPC no longer caches, and Thirdweb + - // Stakeme.pro have come online, restoring 4 clean keyless - // providers. Providers live-verified: sei-official - // (evm-rpc.sei-apis.com), drpc, thirdweb, stakeme. Excluded: - // PublicNode (no sei-evm subdomain), Tenderly public (no - // sei route), BlockPI (Apikey required), MeowRPC (defunct), - // Basement Nodes / Node75 / AllThatNode (host unresolvable - // or key-gated). Sei's dual Cosmos + EVM stack means this bench - // scopes strictly to the EVM side; the Cosmos JSON-RPC would - // need a Kind:"cosmos" entry as we did for Osmosis. + // Providers live-verified: sei-official (evm-rpc.sei-apis.com), + // thirdweb, stakeme. Excluded: dRPC (500 persistent internal error + // after 3 consecutive attempts), PublicNode (no sei-evm subdomain), + // Tenderly public (no sei route), BlockPI (Apikey required), + // MeowRPC (defunct), Basement Nodes / Node75 / AllThatNode + // (host unresolvable or key-gated). Sei's dual Cosmos + EVM stack + // means this bench scopes strictly to the EVM side. { Slug: "sei", Name: "Sei EVM", Providers: []Provider{ {Slug: "sei-official", Name: "Sei Labs", URL: envDefault("RPC_URL_SEI_OFFICIAL", "https://evm-rpc.sei-apis.com")}, - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_SEI_DRPC", "https://sei.drpc.org")}, {Slug: "thirdweb", Name: "Thirdweb", URL: envDefault("RPC_URL_SEI_THIRDWEB", "https://sei.rpc.thirdweb.com")}, {Slug: "stakeme", Name: "Stakeme", URL: envDefault("RPC_URL_SEI_STAKEME", "https://sei-evm-rpc.stakeme.pro")}, }, @@ -641,9 +630,10 @@ func chains() []Chain { }, // ─── Ronin (bench 110) — added 2026-07-26. EVM gaming L1 // (chain 2020) operated by Sky Mavis, primary home of Axie - // Infinity + Pixels + a broader gaming ecosystem. 4 clean + // Infinity + Pixels + a broader gaming ecosystem. 3 clean // keyless providers live-verified: ronin-official - // (api.roninchain.com), drpc, tenderly, thirdweb. Excluded: + // (api.roninchain.com), tenderly, thirdweb. Excluded: + // dRPC (500 persistent internal error after 3 attempts), // Ronin Tech secondary (DNS-fail), PublicNode (no ronin // subdomain), lgns.net (DNS-fail). { @@ -651,7 +641,6 @@ func chains() []Chain { Name: "Ronin", Providers: []Provider{ {Slug: "ronin-official", Name: "Sky Mavis", URL: envDefault("RPC_URL_RONIN_OFFICIAL", "https://api.roninchain.com/rpc")}, - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_RONIN_DRPC", "https://ronin.drpc.org")}, {Slug: "tenderly", Name: "Tenderly Gateway", URL: envDefault("RPC_URL_RONIN_TENDERLY", "https://ronin.gateway.tenderly.co")}, {Slug: "thirdweb", Name: "Thirdweb", URL: envDefault("RPC_URL_RONIN_THIRDWEB", "https://ronin.rpc.thirdweb.com")}, }, @@ -857,11 +846,12 @@ func chains() []Chain { {Slug: "morph-quicknode", Name: "QuickNode", URL: envDefault("RPC_URL_MORPH_QUICKNODE", "https://rpc-quicknode.morph.network")}, }, }, - // 2026-08-03 audit. Moonriver Kusama parachain (chain 1285). 4 clean + // 2026-08-03 audit. Moonriver Kusama parachain (chain 1285). 3 clean // keyless providers: moonriver-official (rpc.api.moonriver.moonbeam.network), // publicnode (moonriver-rpc.publicnode.com), onfinality - // (moonriver.api.onfinality.io/public), unitedbloc (moonriver.unitedbloc.com). - // Excluded: Ankr (key required for moonriver), drpc (no moonriver route). + // (moonriver.api.onfinality.io/public). Excluded: UnitedBloc + // (moonriver.unitedbloc.com DNS dead), Ankr (key required for + // moonriver), drpc (no moonriver route). { Slug: "moonriver", Name: "Moonriver", @@ -869,7 +859,6 @@ func chains() []Chain { {Slug: "moonriver-official", Name: "Moonbeam Foundation", URL: envDefault("RPC_URL_MOONRIVER_OFFICIAL", "https://rpc.api.moonriver.moonbeam.network")}, {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_MOONRIVER_PUBLICNODE", "https://moonriver-rpc.publicnode.com")}, {Slug: "onfinality", Name: "OnFinality", URL: envDefault("RPC_URL_MOONRIVER_ONFINALITY", "https://moonriver.api.onfinality.io/public")}, - {Slug: "unitedbloc", Name: "UnitedBloc", URL: envDefault("RPC_URL_MOONRIVER_UNITEDBLOC", "https://moonriver.unitedbloc.com")}, }, }, // 2026-08-03 audit. Hemi BTC+ETH hybrid OP Stack L2 (chain 43111). 3 clean @@ -987,29 +976,24 @@ func chains() []Chain { {Slug: "ankr", Name: "Ankr", URL: envDefault("RPC_URL_FILECOIN_ANKR", "https://rpc.ankr.com/filecoin")}, }, }, - // 2026-08-04 wave-5. Canto EVM L1 (chain 7700). 4 keyless providers: - // canto-official (canto.gravitychain.io), drpc (canto.drpc.org), - // publicnode (canto-evm.publicnode.com), ankr (rpc.ankr.com/canto). + // 2026-08-04 wave-5. Canto EVM L1 (chain 7700). 1 keyless provider: + // canto-official (canto.gravitychain.io). Excluded: dRPC (404), + // PublicNode (404), Ankr (403 API key required). { Slug: "canto", Name: "Canto", Providers: []Provider{ {Slug: "canto-official", Name: "Canto Official", URL: envDefault("RPC_URL_CANTO_OFFICIAL", "https://canto.gravitychain.io/")}, - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_CANTO_DRPC", "https://canto.drpc.org")}, - {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_CANTO_PUBLICNODE", "https://canto-evm.publicnode.com")}, - {Slug: "ankr", Name: "Ankr", URL: envDefault("RPC_URL_CANTO_ANKR", "https://rpc.ankr.com/canto")}, }, }, - // 2026-08-04 wave-5. Aurora EVM on NEAR Protocol (chain 1313161554). 3 keyless - // providers: aurora-official (mainnet.aurora.dev), drpc (aurora.drpc.org), - // ankr (rpc.ankr.com/aurora). + // 2026-08-04 wave-5. Aurora EVM on NEAR Protocol (chain 1313161554). 1 keyless + // provider: aurora-official (mainnet.aurora.dev). Excluded: Ankr (403 API key + // required), dRPC (403 API key required). { Slug: "aurora", Name: "Aurora", Providers: []Provider{ {Slug: "aurora-official", Name: "Aurora Labs", URL: envDefault("RPC_URL_AURORA_OFFICIAL", "https://mainnet.aurora.dev")}, - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_AURORA_DRPC", "https://aurora.drpc.org")}, - {Slug: "ankr", Name: "Ankr", URL: envDefault("RPC_URL_AURORA_ANKR", "https://rpc.ankr.com/aurora")}, }, }, // 2026-08-04 wave-5. Bitlayer Bitcoin L2 EVM (chain 200901). 3 keyless @@ -1070,25 +1054,24 @@ func chains() []Chain { {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_BOBA_DRPC", "https://boba-eth.drpc.org")}, }, }, - // XDC Network — Enterprise EVM L1 (chain 50). 5 keyless providers. + // XDC Network — Enterprise EVM L1 (chain 50). 4 keyless providers. + // Excluded: dRPC (400 paid plan only). { Slug: "xdc", Name: "XDC Network", Providers: []Provider{ {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_XDC_PUBLICNODE", "https://rpc.xdc.org")}, {Slug: "ankr", Name: "Ankr", URL: envDefault("RPC_URL_XDC_ANKR", "https://rpc.ankr.com/xdc")}, - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_XDC_DRPC", "https://xdc.drpc.org")}, {Slug: "xdc-erpc", Name: "XDC eRPC", URL: envDefault("RPC_URL_XDC_ERPC", "https://erpc.xinfin.network")}, {Slug: "xdc-org", Name: "XDC.org", URL: envDefault("RPC_URL_XDC_ORG", "https://rpc.xdc.org")}, }, }, - // Astar — Polkadot EVM parachain (chain 592). 4 keyless providers. + // Astar — Polkadot EVM parachain (chain 592). 2 keyless providers. + // Excluded: PublicNode (404), BlastAPI (DNS dead). { Slug: "astar", Name: "Astar", Providers: []Provider{ - {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_ASTAR_PUBLICNODE", "https://astar-rpc.publicnode.com")}, - {Slug: "blastapi", Name: "BlastAPI", URL: envDefault("RPC_URL_ASTAR_BLASTAPI", "https://astar.public.blastapi.io")}, {Slug: "onfinality", Name: "OnFinality", URL: envDefault("RPC_URL_ASTAR_ONFINALITY", "https://astar.api.onfinality.io/public")}, {Slug: "1rpc", Name: "1RPC", URL: envDefault("RPC_URL_ASTAR_1RPC", "https://1rpc.io/astr")}, }, @@ -1360,28 +1343,15 @@ func chains() []Chain { {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_KUSAMA_DRPC", "https://kusama.drpc.org")}, }, }, - // Hydration — Polkadot DeFi parachain (HydraDX). 5 keyless providers. - { - Slug: "hydration", - Name: "Hydration", - Kind: "polkadot", - Providers: []Provider{ - {Slug: "hydration-official", Name: "Hydration Foundation", URL: envDefault("RPC_URL_HYDRATION_OFFICIAL", "https://rpc.hydration.cloud")}, - {Slug: "dwellir", Name: "Dwellir", URL: envDefault("RPC_URL_HYDRATION_DWELLIR", "https://hydradx-rpc.dwellir.com")}, - {Slug: "helikon", Name: "Helikon", URL: envDefault("RPC_URL_HYDRATION_HELIKON", "https://rpc.helikon.io/hydradx")}, - {Slug: "dotters-network", Name: "Dotters Network", URL: envDefault("RPC_URL_HYDRATION_DOTTERS", "https://hydradx.dotters.network")}, - {Slug: "parachains-network", Name: "Parachains.network", URL: envDefault("RPC_URL_HYDRATION_PARACHAINS", "https://rpc.parachains.network/hydradx")}, - }, - }, - // ZetaChain — EVM omnichain L1 (chain 7000). 4 keyless providers. + // Hydration — Polkadot DeFi parachain (HydraDX). All providers dead + // (DNS dead or HTTP 400). Chain suspended from active probing. + // ZetaChain — EVM omnichain L1 (chain 7000). 1 keyless provider. + // Excluded: BlockPI (404), AllThatNode (DNS dead). { Slug: "zetachain", Name: "ZetaChain", Providers: []Provider{ - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_ZETACHAIN_DRPC", "https://zetachain.drpc.org")}, - {Slug: "blockpi", Name: "BlockPi", URL: envDefault("RPC_URL_ZETACHAIN_BLOCKPI", "https://zetachain.blockpi.network/v1/rpc/public")}, {Slug: "thirdweb", Name: "Thirdweb", URL: envDefault("RPC_URL_ZETACHAIN_THIRDWEB", "https://7000.rpc.thirdweb.com")}, - {Slug: "allthatnode", Name: "AllThatNode", URL: envDefault("RPC_URL_ZETACHAIN_ATN", "https://zetachain-mainnet.rpc.public.allthatnode.com")}, }, }, // HAQQ — EVM Islamic finance L1 (chain 11235). 3 keyless providers. @@ -1422,14 +1392,13 @@ func chains() []Chain { {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_WEMIX_DRPC", "https://wemix.drpc.org")}, }, }, - // Songbird — Flare canary network (chain 19). 3 keyless providers. + // Songbird — Flare canary network (chain 19). 1 keyless provider. + // Excluded: ftso-au (DNS dead). { Slug: "songbird", Name: "Songbird", Providers: []Provider{ {Slug: "flare-official", Name: "Flare Foundation", URL: envDefault("RPC_URL_SONGBIRD_FLARE", "https://songbird-api.flare.network/ext/C/rpc")}, - {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_SONGBIRD_DRPC", "https://songbird.drpc.org")}, - {Slug: "ftso-au", Name: "FTSO AU", URL: envDefault("RPC_URL_SONGBIRD_FTSOAU", "https://songbird.rpc.ftso.au")}, }, }, // Cronos zkEVM — zkSync-stack Cronos L2 (chain 388). 2 keyless providers. @@ -1451,12 +1420,12 @@ func chains() []Chain { {Slug: "etcmc", Name: "ETCMC", URL: envDefault("RPC_URL_ETC_ETCMC", "https://etcmc.rpc.nz")}, }, }, - // Telos EVM — High-performance EVM (chain 40). 2 keyless providers. + // Telos EVM — High-performance EVM (chain 40). 1 keyless provider. + // Excluded: telos-official (404). { Slug: "telos", Name: "Telos", Providers: []Provider{ - {Slug: "telos-official", Name: "Telos Foundation", URL: envDefault("RPC_URL_TELOS_OFFICIAL", "https://mainnet.telos.net/evm")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_TELOS_DRPC", "https://telos.drpc.org")}, }, }, From c6d912106740f2489aeb7bc748046358d25cf92e Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:48:41 +0200 Subject: [PATCH 08/39] fix: hydration draft+placeholder provider, zetachain/songbird em-dash in findings --- benchmarks/hydration-rpc.yml | 28 +++++++++++++++++++++++++--- benchmarks/songbird-rpc.yml | 2 +- benchmarks/zetachain-rpc.yml | 2 +- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/benchmarks/hydration-rpc.yml b/benchmarks/hydration-rpc.yml index 9fca3db1..61115f2c 100644 --- a/benchmarks/hydration-rpc.yml +++ b/benchmarks/hydration-rpc.yml @@ -8,7 +8,7 @@ seo_description: "Hydration RPC bench temporarily suspended: all 5 providers are subtitle: HTTP round-trip latency for chain_getHeader against every free, no-key public Hydration Polkadot parachain endpoint, audited every 60 seconds from 3 regions. category: RPCs -status: suspended +status: draft metric: RPC latency unit: ms higher_is_better: false @@ -58,7 +58,29 @@ dimensions: - { value: eu-west, label: EU-West } - { value: sgp, label: Singapore } -providers: [] # All 5 previously measured providers are DNS dead or returning HTTP 400. # Suspended 2026-08-07. Will resume when endpoints recover. -# Removed: hydration-official, dwellir, helikon, dotters-network, parachains-network +# Removed: dwellir, helikon, dotters-network, parachains-network +providers: + - slug: hydration-official + name: Hydration + tag: Hydration official public RPC (currently unreachable) + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to the Hydration official no-key endpoint." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="hydration-official", chain="hydration"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="hydration-official", chain="hydration"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="hydration-official", chain="hydration"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="hydration-official", chain="hydration"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="hydration-official", chain="hydration"}) / sum(ocb:rpc_call:rate_24h{provider="hydration-official", chain="hydration"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="hydration-official", chain="hydration"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="hydration-official", chain="hydration"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="hydration-official", chain="hydration", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="hydration-official", chain="hydration", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="hydration-official", chain="hydration", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="hydration-official", chain="hydration", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="hydration-official", chain="hydration", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="hydration-official", chain="hydration", region="sgp"}[1h]) diff --git a/benchmarks/songbird-rpc.yml b/benchmarks/songbird-rpc.yml index cf80f9c5..8510877d 100644 --- a/benchmarks/songbird-rpc.yml +++ b/benchmarks/songbird-rpc.yml @@ -34,7 +34,7 @@ methodology: - "Chain scope: every query on this page is pinned to `chain=\"songbird\"`. Provider coverage: 1 no-key endpoint (Flare Foundation official). FTSO AU removed after DNS went dead." findings: - - "{{best_name}} currently leads free Songbird RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) — 1 measured provider." + - "{{best_name}} currently leads free Songbird RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h), 1 measured provider." faq: - q: "What is the fastest free Songbird RPC right now?" diff --git a/benchmarks/zetachain-rpc.yml b/benchmarks/zetachain-rpc.yml index 5c19737d..9054761d 100644 --- a/benchmarks/zetachain-rpc.yml +++ b/benchmarks/zetachain-rpc.yml @@ -34,7 +34,7 @@ methodology: - "Chain scope: every query on this page is pinned to `chain=\"zetachain\"`. Provider coverage: 1 no-key endpoint (Thirdweb). BlockPI returned 404; AllThatNode DNS is dead." findings: - - "{{best_name}} currently leads free ZetaChain RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) — 1 measured provider." + - "{{best_name}} currently leads free ZetaChain RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h), 1 measured provider." faq: - q: "What is the fastest free ZetaChain RPC right now?" From c534730e09b58402592ddd3853b440d4f221abd6 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:56:22 +0200 Subject: [PATCH 09/39] fix: hydration draft+placeholder provider, zetachain/songbird em-dash in findings (#1794) --- benchmarks/hydration-rpc.yml | 28 +++++++++++++++++++++++++--- benchmarks/songbird-rpc.yml | 2 +- benchmarks/zetachain-rpc.yml | 2 +- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/benchmarks/hydration-rpc.yml b/benchmarks/hydration-rpc.yml index 9fca3db1..61115f2c 100644 --- a/benchmarks/hydration-rpc.yml +++ b/benchmarks/hydration-rpc.yml @@ -8,7 +8,7 @@ seo_description: "Hydration RPC bench temporarily suspended: all 5 providers are subtitle: HTTP round-trip latency for chain_getHeader against every free, no-key public Hydration Polkadot parachain endpoint, audited every 60 seconds from 3 regions. category: RPCs -status: suspended +status: draft metric: RPC latency unit: ms higher_is_better: false @@ -58,7 +58,29 @@ dimensions: - { value: eu-west, label: EU-West } - { value: sgp, label: Singapore } -providers: [] # All 5 previously measured providers are DNS dead or returning HTTP 400. # Suspended 2026-08-07. Will resume when endpoints recover. -# Removed: hydration-official, dwellir, helikon, dotters-network, parachains-network +# Removed: dwellir, helikon, dotters-network, parachains-network +providers: + - slug: hydration-official + name: Hydration + tag: Hydration official public RPC (currently unreachable) + formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `chain_getHeader` POST sent every 60s from 3 regions to the Hydration official no-key endpoint." + queries: + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="hydration-official", chain="hydration"}) + p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="hydration-official", chain="hydration"}) + p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="hydration-official", chain="hydration"}) + mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="hydration-official", chain="hydration"}) + success: sum(ocb:rpc_call:ok_rate_24h{provider="hydration-official", chain="hydration"}) / sum(ocb:rpc_call:rate_24h{provider="hydration-official", chain="hydration"}) + sample_size: sum(ocb:rpc_call:increase_24h{provider="hydration-official", chain="hydration"}) + series: avg(avg_over_time(rpc_latency_milliseconds{provider="hydration-official", chain="hydration"}[1h])) + regions: + - region: us-east + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="hydration-official", chain="hydration", region="us-east"}) + series: avg_over_time(rpc_latency_milliseconds{provider="hydration-official", chain="hydration", region="us-east"}[1h]) + - region: eu-west + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="hydration-official", chain="hydration", region="eu-west"}) + series: avg_over_time(rpc_latency_milliseconds{provider="hydration-official", chain="hydration", region="eu-west"}[1h]) + - region: ap-southeast + p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="hydration-official", chain="hydration", region="sgp"}) + series: avg_over_time(rpc_latency_milliseconds{provider="hydration-official", chain="hydration", region="sgp"}[1h]) diff --git a/benchmarks/songbird-rpc.yml b/benchmarks/songbird-rpc.yml index cf80f9c5..8510877d 100644 --- a/benchmarks/songbird-rpc.yml +++ b/benchmarks/songbird-rpc.yml @@ -34,7 +34,7 @@ methodology: - "Chain scope: every query on this page is pinned to `chain=\"songbird\"`. Provider coverage: 1 no-key endpoint (Flare Foundation official). FTSO AU removed after DNS went dead." findings: - - "{{best_name}} currently leads free Songbird RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) — 1 measured provider." + - "{{best_name}} currently leads free Songbird RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h), 1 measured provider." faq: - q: "What is the fastest free Songbird RPC right now?" diff --git a/benchmarks/zetachain-rpc.yml b/benchmarks/zetachain-rpc.yml index 5c19737d..9054761d 100644 --- a/benchmarks/zetachain-rpc.yml +++ b/benchmarks/zetachain-rpc.yml @@ -34,7 +34,7 @@ methodology: - "Chain scope: every query on this page is pinned to `chain=\"zetachain\"`. Provider coverage: 1 no-key endpoint (Thirdweb). BlockPI returned 404; AllThatNode DNS is dead." findings: - - "{{best_name}} currently leads free ZetaChain RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) — 1 measured provider." + - "{{best_name}} currently leads free ZetaChain RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h), 1 measured provider." faq: - q: "What is the fastest free ZetaChain RPC right now?" From 8a11c6779fcfc22ace839f7e2106237f0da3d6b7 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:42:50 +0200 Subject: [PATCH 10/39] =?UTF-8?q?feat:=20add=20/apps=20leaderboard=20found?= =?UTF-8?q?ation=20=E2=80=94=20specs,=20Go=20harness,=20Postgres=20schema,?= =?UTF-8?q?=20alerting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- benchmarks/apps/04-addendum.md | 112 ++++++++++ benchmarks/apps/_taxonomy.yaml | 111 ++++++++++ benchmarks/apps/aerodrome.yaml | 132 ++++++++++++ benchmarks/apps/dydx-v4.yaml | 144 +++++++++++++ benchmarks/apps/gmx-v2.yaml | 148 +++++++++++++ benchmarks/apps/hyperliquid.yaml | 175 +++++++++++++++ benchmarks/apps/raydium.yaml | 154 ++++++++++++++ benchmarks/apps/uniswap.yaml | 201 ++++++++++++++++++ harnesses/apps/Dockerfile | 13 ++ harnesses/apps/cmd/collector/main.go | 87 ++++++++ harnesses/apps/cmd/materializer/main.go | 68 ++++++ harnesses/apps/docker-compose.apps.yml | 65 ++++++ harnesses/apps/env.apps.example | 32 +++ harnesses/apps/go.mod | 26 +++ harnesses/apps/go.sum | 52 +++++ harnesses/apps/internal/collect/rest/dydx.go | 199 +++++++++++++++++ harnesses/apps/internal/invariant/checks.go | 130 +++++++++++ harnesses/apps/internal/ledger/ledger.go | 148 +++++++++++++ harnesses/apps/internal/spec/types.go | 52 +++++ harnesses/apps/migrations/001_init.sql | 181 ++++++++++++++++ .../apps/prom-rules/alert_rules_apps.yml | 151 +++++++++++++ 21 files changed, 2381 insertions(+) create mode 100644 benchmarks/apps/04-addendum.md create mode 100644 benchmarks/apps/_taxonomy.yaml create mode 100644 benchmarks/apps/aerodrome.yaml create mode 100644 benchmarks/apps/dydx-v4.yaml create mode 100644 benchmarks/apps/gmx-v2.yaml create mode 100644 benchmarks/apps/hyperliquid.yaml create mode 100644 benchmarks/apps/raydium.yaml create mode 100644 benchmarks/apps/uniswap.yaml create mode 100644 harnesses/apps/Dockerfile create mode 100644 harnesses/apps/cmd/collector/main.go create mode 100644 harnesses/apps/cmd/materializer/main.go create mode 100644 harnesses/apps/docker-compose.apps.yml create mode 100644 harnesses/apps/env.apps.example create mode 100644 harnesses/apps/go.mod create mode 100644 harnesses/apps/go.sum create mode 100644 harnesses/apps/internal/collect/rest/dydx.go create mode 100644 harnesses/apps/internal/invariant/checks.go create mode 100644 harnesses/apps/internal/ledger/ledger.go create mode 100644 harnesses/apps/internal/spec/types.go create mode 100644 harnesses/apps/migrations/001_init.sql create mode 100644 harnesses/apps/prom-rules/alert_rules_apps.yml diff --git a/benchmarks/apps/04-addendum.md b/benchmarks/apps/04-addendum.md new file mode 100644 index 00000000..9ac40f78 --- /dev/null +++ b/benchmarks/apps/04-addendum.md @@ -0,0 +1,112 @@ +# `/apps` — Addendum aux décisions ouvertes + +**Date : 9 août 2026.** Ce document tranche les trois points laissés ouverts dans +`00-EXECUTION-BRIEF.md` et corrige un passage approximatif sur le budget de recompute. + +--- + +## Décision 1 — Renommage de la colonne de classement + +`net_real_revenue` est un abus de langage dès qu'on y inclut des buybacks : ce n'est pas +du revenue au sens comptable. Le terme induit le même péché de fusion que DefiLlama. + +**Décision** : la colonne de classement s'appelle `net_value_capture_usd`, libellé UI +**« Valeur nette captée »**. La formule est affichée **en permanence** sous l'en-tête de +colonne (pas au survol) : + +``` +Valeur nette captée += trésor + holders + burn − émissions +``` + +La barre segmentée décompose visuellement les trois termes positifs. Le chiffre scalaire +reste disponible pour le tri mais n'est jamais présenté sans sa formule. + +**Cohérence avec P4** : `with_holders` est le défaut. `treasury_only` est disponible en +toggle discret. Les deux variantes sont calculées et stockées dans `fee_facts_windowed` +sous `variant IN ('with_holders', 'treasury_only')`. + +--- + +## Décision 2 — Budget de recompute : critère chiffré + +La formulation « c'est un simple GROUP BY, ça tient en 30 minutes » était paresseuse. Le +vrai goulot d'étranglement est la jointure de pricing sur `(token, hour)`. + +**Analyse** : + +- Seules les paires `(token, heure)` où un event a eu lieu sont pricées : la table de + prix est **creuse et pilotée par la demande**, pas dense. +- Les prix sont une observation (pas un artefact de méthodologie) : un recompute de + formule ne les invalide pas. Seul un changement de méthode de pricing le fait, et c'est + rare. +- Le long tail Solana (memecoins) tombe en `pool_ratio`, dérivé du swap lui-même. Ce sont + les cas les moins chers : aucun appel externe. Seuls ~50 tokens majeurs touchent Pyth. + +**Critère d'acceptation chiffré (Phase 1)** : + +``` +Benchmark sur 20M d'events synthétiques, 3 000 tokens distincts : +- Recompute complet < 30 minutes sur le VPS (4 cores, 15GB RAM) +- EXPLAIN ANALYZE ne montre aucun Seq Scan sur fee_events ou fee_facts +- Si ces deux conditions ne sont pas satisfaites simultanément : + partitionner fee_events par mois (PARTITION BY RANGE sur ts) + et fee_facts par (bucket_start, methodology_version) +``` + +Le partitionnement n'est pas activé par défaut : il complique le schéma sans bénéfice +prouvé à l'échelle MVP. Le benchmark tranche, pas le raisonnement. + +--- + +## Décision 3 — Aerodrome en T1, phase 4a + +**Constat** : indexer les swaps Aerodrome en direct getLogs est intenable (milliers de +pools × millions de swaps). Mais on n'a pas besoin des swaps. + +Les fees d'Aerodrome atterrissent dans les contrats `FeesVotingReward` (un par gauge), +via `notifyRewardAmount()` appelé par le Voter. L'event émis est : + +```solidity +// IReward.sol — vérifié sur aerodrome-finance/contracts main +event NotifyReward( + address indexed from, // Voter + address indexed reward, // token de fee (USDC, WETH, etc.) + uint256 indexed epoch, // epochStart en secondes + uint256 amount // montant en unités natives +); + +// topic0 vérifié : +// 0x52977ea98a2220a03ee9ba5cb003ada08d394ea10155483c95dc2dc77a7eb24b +``` + +Volume réel : O(epoch × gauges actives × tokens) — quelques centaines d'events par +semaine, pas un par swap. **Aerodrome est indexable en T1 sans Envio.** + +**Prix à payer documenté** : granularité epoch (hebdomadaire). La fenêtre 24h d'Aerodrome +est de résolution dégradée — l'émission AERO est répartie uniformément sur 7 jours +(cf. `aerodrome.yaml : spread_over_epoch: true`) mais les fees sont discrets. L'UI affiche +un badge « résolution hebdomadaire » sur la ligne Aerodrome pour les fenêtres 24h et 7j. +La fenêtre 30j est pleine résolution. + +**Comment découvrir les FeesVotingReward** : via `Voter.gaugeToFees(gaugeAddress)`. +Énumérer les gauges actifs depuis `Voter`, résoudre leur contrat de fees à l'init, +surveiller les nouveaux gauges à chaque epoch. + +**Aerodrome passe de Phase 4 à Phase 4a** (parallèle à Uniswap, avant Raydium qui reste +le plus complexe). + +--- + +## Correction — passage approximatif dans 01-architecture.md + +Le §7 (séquencement) listait Raydium avant Uniswap. Ordre corrigé : + +| Phase | Contenu | +|---|---| +| 1 | Socle + dYdX | +| 2 | Hyperliquid + GMX V2 | +| 3 | Page `/apps` + API publique (3 protocoles) | +| 4a | Aerodrome (T1 via NotifyReward) + Uniswap | +| 4b | Raydium (le plus complexe : accrual Solana) | +| 5 | Changelog, exports CSV, coverage page | diff --git a/benchmarks/apps/_taxonomy.yaml b/benchmarks/apps/_taxonomy.yaml new file mode 100644 index 00000000..6aa2887f --- /dev/null +++ b/benchmarks/apps/_taxonomy.yaml @@ -0,0 +1,111 @@ +# Taxonomie partagée par toutes les specs /apps. +# Un fee_event ne peut porter qu'un `component` et qu'un `beneficiary` de ces listes. +# Toute modification ici est un changement de méthodologie : bump obligatoire de +# methodology_version sur les specs affectées + recompute complet. + +schema_version: 1 + +components: + taker_fee: + included_in_gross: true + label: "Frais preneur" + maker_fee: + included_in_gross: true + label: "Frais offreur" + maker_rebate: + included_in_gross: false + label: "Rabais offreur" + note: "Montant stocké positif, beneficiary=lp. Sortie de trésorerie, pas un fee." + swap_fee: + included_in_gross: true + label: "Frais de swap AMM" + borrow_fee: + included_in_gross: true + label: "Frais d'emprunt" + note: "Payé au pool, pas à une contrepartie. Revenue réel du système." + liquidation_fee: + included_in_gross: true + label: "Frais de liquidation" + note: "Uniquement la part retenue par le protocole ou le pool. Le collatéral saisi est une perte du trader, jamais un fee." + pool_creation_fee: + included_in_gross: true + label: "Frais de création de pool" + bonding_curve_fee: + included_in_gross: true + label: "Frais de bonding curve" + + # --- Collectés mais exclus des agrégats par défaut --- + funding_payment: + included_in_gross: false + toggleable: true + label: "Paiements de funding" + rationale: >- + Transfert de somme nulle entre longs et shorts. Le protocole n'en retient rien. + L'inclure gonfle mécaniquement les perps face aux DEX spot et rend le classement + inter-catégories inutilisable. + interface_fee: + included_in_gross: true + beneficiary_forced: third_party + is_app_layer: true + excluded_from_ranking: true + rationale: >- + Réel, mais Uniswap Labs a coupé ses frontend fees lors d'UNIfication. Les compter + chez les autres fausserait la comparaison. Mesuré, affiché, hors classement. + external_bribe: + included_in_gross: false + label: "Incentives externes" + rationale: "Payé par des tiers aux voters, pas un revenue généré par le protocole." + sequencer_revenue: + included_in_gross: false + counted_in: non_trading_holder_value + rationale: >- + Cas Unichain : entre dans le même mécanisme de burn que les protocol fees, mais + n'est pas un frais de trading. Additionné séparément et visiblement. + +exclusions: + # Jamais collectés. Documentés pour que la page méthodologie soit complète. + - id: price_impact + reason: "Capté par l'inventaire du LP, non identifiable comme fee dans les events." + - id: mev + reason: "Va au searcher et au validateur, pas au protocole." + - id: gas_and_priority_fees + reason: "Coût réseau, pas revenue protocole." + - id: seized_collateral + reason: "Perte du trader, pas un frais payé au système." + - id: treasury_one_offs + reason: >- + Burns rétroactifs et distributions exceptionnelles (ex. les 100M UNI du trésor + lors d'UNIfication). Ce n'est pas du revenue récurrent ; l'inclure produirait des + pics absurdes. + +beneficiaries: + lp: { label: "Fournisseurs de liquidité", in_protocol_cut: false, in_holder_value: false } + token_holder: { label: "Détenteurs du token", in_protocol_cut: false, in_holder_value: true } + staker: { label: "Stakers / validateurs", in_protocol_cut: false, in_holder_value: true } + burn: { label: "Brûlé", in_protocol_cut: false, in_holder_value: true } + treasury: { label: "Trésor", in_protocol_cut: true, in_holder_value: false } + insurance_fund: { label: "Fonds d'assurance", in_protocol_cut: true, in_holder_value: false } + third_party: { label: "Tiers (frontends, affiliés)", in_protocol_cut: false, in_holder_value: false } + creator: { label: "Créateur du pool / token", in_protocol_cut: false, in_holder_value: false } + +# Métriques dérivées. Ce sont des VUES sur la partition ci-dessus, pas des colonnes +# indépendantes. Aucune ne peut compter deux fois le même event. +derived: + gross_fees: "Σ components[included_in_gross]" + lp_revenue: "Σ beneficiary == lp" + protocol_cut: "Σ beneficiary.in_protocol_cut" + token_holder_value: "Σ beneficiary.in_holder_value" + emissions_cost: "Σ_jours tokens_émis(j) × TWAP_usd(j)" + net_real_revenue: + with_holders: "protocol_cut + token_holder_value + non_trading_holder_value − emissions_cost" + treasury_only: "protocol_cut − emissions_cost" + default: with_holders + note: >- + `treasury_only` correspond à la définition littérale du brief mais classe Hyperliquid + comme quasi nul, ce qui contredit l'intention. `with_holders` produit le classement + décrit. Les deux sont calculées ; à trancher avant le lancement public. + +invariants: + unallocated_tolerance_bps: 50 + witness_divergence_warn_bps: 200 + witness_divergence_quarantine_bps: 500 diff --git a/benchmarks/apps/aerodrome.yaml b/benchmarks/apps/aerodrome.yaml new file mode 100644 index 00000000..2f6c9e3f --- /dev/null +++ b/benchmarks/apps/aerodrome.yaml @@ -0,0 +1,132 @@ +schema_version: 1 +slug: aerodrome +name: Aerodrome +category: dex +methodology_version: 1 +status: live +verifiability: tier_2 + +# Le protocole qui justifie la colonne net_real_revenue. +# Résultat attendu : NÉGATIF. Ce n'est pas une anomalie à corriger. + +deployments: + - id: aerodrome:base + chain: base + effective_from: "2023-08-28T00:00:00Z" + + finality: + mode: l1_confirmed + lag_seconds: 900 + + source: + kind: envio_hyperindex + canonical: true + repo: https://github.com/velodrome-finance/indexer + note: >- + T2 : dépendance à un indexer tiers. Migration vers evm_logs directs prévue + en phase 5. L'architecture ve(3,3) rend cette dépendance structurellement + risquée à long terme. + + witnesses: + - kind: defillama + endpoint: https://api.llama.fi/summary/fees/aerodrome + expected_divergence: high + note: >- + DefiLlama ne soustrait pas les émissions. La divergence est attendue et + constitue la démonstration de la thèse du produit — ne pas la traiter + comme une erreur à réconcilier. + +allocation: + rules: + - source: pool_fees + splits: + - component: swap_fee + beneficiary: token_holder # 100% aux voters veAERO + share_source: onchain_field + field: fee_amount + - source: slipstream_unstaked + splits: + - component: swap_fee + beneficiary: lp + share_source: onchain_field + field: unstaked_fee + note: "Positions CL non-stakées : traitement distinct, ne pas confondre." + - source: external_bribes + splits: + - component: external_bribe # exclu de gross_fees par la taxonomie + beneficiary: token_holder + share_source: onchain_field + note: "Payé par des tiers. L'inclure gonflerait artificiellement Aerodrome." + + # Exemple de constante correctement encodée : bornée dans le temps, sourcée. + # Le CI échoue si source_url ou effective_from manque, ou si deux intervalles + # se chevauchent. C'est le garde-fou anti-Lido. + constants: + - name: treasury_share + value: 0.0 + effective_from: "2023-08-28T00:00:00Z" + effective_to: null + source_url: https://aerodrome.finance/docs + review_due: "2026-11-01" + +pricing: + primary: pyth_hermes + witness: chainlink + granularity: hourly_twap + fallback: pool_ratio + fallback_confidence: low + note: "Milliers de tokens sur Base. La couverture Pyth sera partielle." + +emissions: + scope: incentives_only + source: + kind: evm_logs + contract: minter + events: [Mint] + read: distributions_to_gauges + note: >- + On mesure les AERO effectivement distribués aux gauges, epoch par epoch. + PAS un taux d'inflation annualisé appliqué à la supply : le "~10.9%/an" + couramment cité est un taux d'inflation, pas un coût d'incentive, et + l'approximation ne résiste pas à un examen sérieux. + token: AERO + epoch: weekly + spread_over_epoch: true + spread_note: >- + Émissions discrètes (hebdomadaires) réparties uniformément sur 7 jours pour les + fenêtres 24h et 7j. Sans ça, le classement 24h dépendrait du jour de l'epoch. + Choix documenté et affiché sur la page méthodologie. + excluded: + - id: ve_rebase + reason: >- + Anti-dilution des lockers, pas une incentive d'acquisition de volume. + Inclus uniquement sous scope=all_inflation. + +validation: + checks: + - id: minter_conservation + kind: onchain_conservation + description: "Émissions du Minter par epoch == Σ distributions aux gauges" + tolerance_bps: 1 + note: "Doit correspondre exactement. Un écart signale un gauge manqué." + - id: voter_fee_conservation + description: "Fees distribués aux voters == Σ fees des pools" + tolerance_bps: 100 + +ui: + # Sans cette note, la ligne "lp_revenue = 0" se lit comme une donnée manquante. + annotations: + - target: lp_revenue + text: >- + Les LPs Aerodrome ne perçoivent pas de fees : ils reçoivent des émissions AERO + à la place. La valeur leur revenant est réelle mais dilutive, et apparaît en + coût d'émission plutôt qu'en revenue. + - target: net_real_revenue + text: >- + Négatif : le protocole distribue plus de valeur en émissions qu'il ne capte + en frais. C'est un résultat mesuré, pas une erreur de données. + +metadata: + chains: [base] + is_router: false + nests_within: null diff --git a/benchmarks/apps/dydx-v4.yaml b/benchmarks/apps/dydx-v4.yaml new file mode 100644 index 00000000..4a6a0e47 --- /dev/null +++ b/benchmarks/apps/dydx-v4.yaml @@ -0,0 +1,144 @@ +schema_version: 1 +slug: dydx-v4 +name: dYdX V4 +category: perps +methodology_version: 1 +status: live +verifiability: tier_2 + +# Premier protocole implémenté : REST + tout en USDC, pas d'oracle. +# Valide toute la chaîne (ledger, materializer, invariants, quarantaine) sans bruit. + +deployments: + - id: dydx-v4:dydx-chain + chain: dydx-chain + effective_from: "2023-10-26T00:00:00Z" + + finality: + mode: cosmos_finalized + lag_seconds: 10 + + source: + kind: rest + canonical: true + base_url: "https://indexer.dydx.trade/v4" + endpoints: + - /fills # source primaire + - /historicalTradingRewardAggregations/{address} + - /vault/v1/megavault/historicalPnl + pagination: by_height # curseur createdAtHeight, jamais offset temporel + note: >- + T2 : dépendance à l'indexer dYdX. Plan B : full node + indexer self-hosted + (open-source, Postgres + Redis). Objectif à 6 mois. + + witnesses: + - kind: defillama + endpoint: https://api.llama.fi/summary/fees/dydx + - kind: onchain_megavault + description: >- + Inflows USDC vers l'adresse MegaVault ≈ 50% de Σ fills.fee. + Témoin purement on-chain, indépendant de l'indexer. + +allocation: + # ATTENTION : lire on-chain via x/revshare avant de coder. + # "100% validators/stakers, 0% trésor" était faux depuis dYdX Unlimited. + rules: + - component: taker_fee + splits: + - beneficiary: lp + share_source: onchain_config + read: revshare_megavault_fraction # ~50% + param: megavault_taker_fee_share + - beneficiary: treasury + share_source: onchain_config + read: revshare_treasury_fraction # ~10% + param: treasury_taker_fee_share + - beneficiary: staker + share_source: derived # reste (~40%) + - beneficiary: third_party + component: affiliate_fee + share_source: onchain_field + field: affiliateRevShare + note: >- + Affiliés VIP jusqu'à 50% du taker fee du trader référé. + Décomposer PAR FILL, jamais en moyenne. + + - component: maker_fee + splits: + - beneficiary: lp + share_source: onchain_config + read: revshare_megavault_fraction + - beneficiary: staker + share_source: derived + + # Rebates maker (fee négatif dans fills.fee) + - component: maker_rebate + included_in_gross: false + beneficiary: lp + note: "Montant positif stocké, gross_fees = fees bruts payés avant rebate." + + # Paramètres lus quotidiennement depuis x/revshare, versionnés dans allocation_params. + # La contrainte GIST empêche deux valeurs simultanées pour le même param. + params_source: + kind: cosmos_grpc + module: x/revshare + snapshot: daily + note: "Le lire, pas le supposer. Les paramètres changent par vote de gouvernance." + + documented_split_reference: + megavault: 0.50 + treasury_subdao: 0.10 + market_mapper: 0.00 + stakers: 0.40 + source_url: https://dydx.exchange/blog/dydx-unlimited + used_for: plausibility_check_only + alert_if_deviation_gt: 0.05 + +pricing: + fees_denomination: USDC + primary: stable_passthrough + depeg_guard_bps: 50 + note: "Tout en USDC. Pas d'oracle nécessaire pour les fees." + +emissions: + scope: incentives_only + sources: + - kind: megavault_rewards + token: DYDX + note: "Rewards MegaVault en DYDX. Zéro-somme comptablement mais coût réel." + - kind: trading_rewards + token: DYDX + status: onchain_param + note: >- + Statut = paramètre de gouvernance. Le lire via x/rewards avant chaque recompute. + Ne jamais supposer actif ou inactif. + +validation: + checks: + - id: megavault_inflows + kind: onchain_conservation + description: "Inflows USDC vers MegaVault ≈ 50% de Σ fills.fee sur 24h" + tolerance_bps: 300 + note: "Meilleur test disponible : purement on-chain, indépendant de l'indexer." + - id: boundary_test + kind: unit_test + description: "3 events à t1-1ms / t1 / t1+1ms : fenêtre [t0,t1) retourne exactement 1" + severity: blocking + - id: idempotence + kind: unit_test + description: "Injecter deux fois le même event ne change aucun chiffre" + severity: blocking + - id: recompute_speed + kind: perf + description: "Recompute complet historique dYdX < 5 minutes" + - id: quarantine_trigger + kind: integration + description: "Écart artificiel de 6% injecté dans le témoin déclenche bien la quarantaine" + severity: blocking + +metadata: + chains: [dydx-chain] + is_router: false + nests_within: null + docs: https://docs.dydx.exchange + indexer: https://indexer.dydx.trade/v4 diff --git a/benchmarks/apps/gmx-v2.yaml b/benchmarks/apps/gmx-v2.yaml new file mode 100644 index 00000000..f792ef2c --- /dev/null +++ b/benchmarks/apps/gmx-v2.yaml @@ -0,0 +1,148 @@ +schema_version: 1 +slug: gmx-v2 +name: GMX V2 +category: perps +methodology_version: 1 +status: live +website: https://gmx.io +verifiability: tier_1 + +# Exemple canonique : TOUS les splits sont lus dans l'event, aucune constante. +# C'est le pattern à copier. Voir aerodrome.yaml pour le cas où c'est impossible. + +deployments: + - id: gmx-v2:arbitrum + chain: arbitrum + effective_from: "2023-08-01T00:00:00Z" + start_block: 107737756 + + finality: + mode: l1_confirmed + lag_seconds: 900 + + source: + kind: evm_logs + canonical: true + address: "0xC8ee91A54287DB53897056e12D9819156D3822Fb" # EventEmitter + # topic0 de EventLog1. Constante du contrat, pas un paramètre métier. + topic0: "0x137a44067c8961cd7e1d876f4754a5a3a75989b4552f1843fc69c3b372def160" + # topic1 = keccak256(nom). Calculé AU RUNTIME. Un hash collé en dur et mal + # recopié retourne zéro event, ce qui est indiscernable de "pas d'activité". + event_names: + - PositionFeesCollected + - SwapFeesCollected + decoder: gmx_v2_eventlog # payload = key-value typés, décodage par clé + batch_blocks: 2000 + + witnesses: + - kind: subgraph + id: gmx-v2-arbitrum + - kind: defillama + endpoint: https://api.llama.fi/summary/fees/gmx-v2 + note: "Adapter écrit par le protocole. Témoin, jamais source." + + - id: gmx-v2:avalanche + chain: avalanche + effective_from: "2023-08-01T00:00:00Z" + # Déploiement distinct, pot de fees distinct : la somme avec Arbitrum est + # correcte et ne constitue pas un double-comptage (cf. architecture §6.5). + source: + kind: evm_logs + canonical: true + address: "TODO_verifier_avant_activation" + topic0: "0x137a44067c8961cd7e1d876f4754a5a3a75989b4552f1843fc69c3b372def160" + event_names: [PositionFeesCollected, SwapFeesCollected] + decoder: gmx_v2_eventlog + status: pending_verification + +allocation: + rules: + - event: PositionFeesCollected + splits: + - field: positionFeeAmount + component: taker_fee + share_source: onchain_field # ← jamais un pourcentage + - field: borrowingFeeAmount + component: borrow_fee + share_source: onchain_field + - field: fundingFeeAmount + component: funding_payment # exclu de gross_fees par la taxonomie + share_source: onchain_field + - field: feeAmountForPool + beneficiary: lp + share_source: onchain_field + - field: feeReceiverAmount + beneficiary: token_holder # rachat GMX + share_source: onchain_field + - field: uiFeeAmount + beneficiary: third_party + component: interface_fee + share_source: onchain_field + + - event: SwapFeesCollected + splits: + - field: feeAmountForPool + component: swap_fee + beneficiary: lp + share_source: onchain_field + - field: feeReceiverAmount + component: swap_fee + beneficiary: token_holder + share_source: onchain_field + + # Contrôle intra-event. Attrape la majorité des régressions de décodage : + # si GMX ajoute un champ entre deux versions du contrat, cette assertion échoue + # avant que le chiffre ne soit publié. + assertions: + - name: position_fee_conservation + expr: "positionFeeAmount == feeAmountForPool + feeReceiverAmount + uiFeeAmount" + on_failure: quarantine + + # Le 63/27/10 documenté n'est JAMAIS appliqué au calcul. Il sert uniquement de + # test de plausibilité : un écart persistant est une information publiable. + documented_split_reference: + lp: 0.63 + token_holder: 0.27 + overhead: 0.10 + source_url: https://docs.gmx.io/docs/tokenomics + used_for: plausibility_check_only + alert_if_deviation_gt: 0.05 + +pricing: + # Collatéral mixte : conversion obligatoire. + primary: pyth_hermes + witness: chainlink + granularity: hourly_twap + chainlink_guards: + - sequencer_uptime_feed: required # Arbitrum : prix invalides si le sequencer était down + fallback: pool_ratio + fallback_confidence: low + +emissions: + scope: incentives_only + sources: + - kind: esgmx_distribution + token: GMX + accounting: at_emission # au moment de l'émission, pas du vesting + note: "Choix documenté : le coût est encouru quand le token est émis." + - kind: external_grant + token: ARB + note: >- + Programmes d'incentives Arbitrum. Les omettre flatterait net_real_revenue. + Source : distributions ARB vers les contrats GMX. + status: todo + +validation: + checks: + - id: fee_receiver_inflows + kind: onchain_conservation + description: "Inflows vers le fee receiver GMX ≈ Σ feeReceiverAmount" + tolerance_bps: 200 + - id: witness_divergence + kind: witness + quarantine_above_bps: 500 + +metadata: + chains: [arbitrum, avalanche] + is_router: false + nests_within: null diff --git a/benchmarks/apps/hyperliquid.yaml b/benchmarks/apps/hyperliquid.yaml new file mode 100644 index 00000000..f095932f --- /dev/null +++ b/benchmarks/apps/hyperliquid.yaml @@ -0,0 +1,175 @@ +schema_version: 1 +slug: hyperliquid +name: Hyperliquid +category: perps +methodology_version: 1 +status: live +verifiability: tier_2 # -> tier_1 une fois le node en place + +# Cas non-EVM. La question "plan B si l'API tombe ?" a une vraie réponse : le node. +# L'archive S3 et le node partagent le MÊME FORMAT, donc le décodeur est commun et +# bascule sans changement de code. Une panne REST coûte la fraîcheur, pas l'historique. + +deployments: + - id: hyperliquid:hypercore + chain: hyperliquid + effective_from: "2023-06-01T00:00:00Z" + + finality: + mode: hyperbft + lag_seconds: 5 + note: "Finalité quasi immédiate, mais la dépendance porte sur la source de données." + + # Sources par ordre de préférence. Le collector bascule automatiquement. + sources: + - kind: hl_node + rank: 1 + canonical: true + flags: ["--write-fills", "--batch-by-block"] + note: "Seule configuration T1. Objectif cible à 6 mois." + status: planned + + - kind: s3_archive + rank: 2 + canonical: true + buckets: + fills: "s3://hl-mainnet-node-data/node_fills_by_block" + misc: "s3://hl-mainnet-node-data/misc_events_by_block" + requester_pays: true + cost_warning: >- + ~0.8-1 GiB/jour tous coins confondus. Un backfill d'un an est une facture AWS + à trois chiffres. BUDGÉTER, et ne backfiller qu'une seule fois. + note: "Format identique à l'API. misc_events couvre transferts, staking, funding." + + - kind: rest + rank: 3 + endpoint: "https://api.hyperliquid.xyz/info" + method: POST + types: [metaAndAssetCtxs, fundingHistory, clearinghouseState, builderFills] + note: "Temps réel et métadonnées. Bornes INCLUSIVES : appeler à t1-1ms + re-filtrer." + + witnesses: + - kind: defillama + endpoint: https://api.llama.fi/summary/fees/hyperliquid + - kind: onchain_af_inflows + address: "0xfefefefefefefefefefefefefefefefefefefefe" + note: "Le meilleur témoin : purement observationnel, indépendant de l'API." + +allocation: + # ATTENTION : le brief d'origine disait "99% AF / 1% HLP". C'est incomplet ET imprécis. + # (a) L'allocation de référence est ~97%, portée vers 99% pour CERTAINES catégories + # de fees par un vote de validateurs de décembre 2025. + # (b) Le brief OMET ENTIÈREMENT les déployeurs spot et HIP-3. + rules: + - component: taker_fee + splits: + - beneficiary: burn + share_source: onchain_config + read: assistance_fund_share + note: >- + Le HYPE détenu par l'AF est formellement reconnu comme brûlé depuis un vote + pondéré par le stake (85% pour) de décembre 2025. `burn` est donc correct, + pas `token_holder`. + - beneficiary: lp + share_source: derived + target: hlp_vault + + - component: maker_fee + splits: + - beneficiary: burn + share_source: onchain_config + read: assistance_fund_share + - beneficiary: lp + share_source: derived + + # JAMBE MANQUANTE DANS LE BRIEF. Obligatoire même si elle vaut zéro. + - component: taker_fee + scope: hip3_and_spot_deployed_assets + splits: + - beneficiary: third_party + share_source: onchain_field + read: deployer_fee_share + max_observed: 0.50 + note: >- + Les déployeurs spot et HIP-3 peuvent conserver jusqu'à 50% des fees générés + par LEURS assets. Les ignorer surestime le protocole. + + # Builder fees : captés par les frontends tiers, séparés des fees protocole. + - component: interface_fee + source: builderFills + splits: + - beneficiary: third_party + is_app_layer: true + share_source: onchain_field + + documented_split_reference: + assistance_fund: 0.97 + source_url: https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees + used_for: plausibility_check_only + alert_if_deviation_gt: 0.01 + note: >- + SI L'ÉCART DÉPASSE 1%, L'ALLOCATION A CHANGÉ SANS ANNONCE. C'est exactement le + signal que le produit est censé détecter. + +pricing: + # Les fees sont en USDC : pas d'oracle nécessaire pour les fees eux-mêmes. + fees_denomination: USDC + primary: stable_passthrough + depeg_guard_bps: 50 + # En revanche les oraclePx de HL sont self-reported -> triangulation obligatoire. + oracle_triangulation: + against: pyth_hermes + assets: [BTC, ETH, SOL] + alert_if_divergence_gt: 0.01 + note: "Aucun tiers ne peut vérifier les prix de HL. Trianguler ou ne pas publier." + +emissions: + scope: incentives_only + total: 0 + review_due: "2026-11-01" + note: >- + Zéro émission d'incentive. À REVÉRIFIER TRIMESTRIELLEMENT, pas à supposer acquis. + Les unlocks mensuels aux contributeurs (~9.92M HYPE) sont de la dilution, pas un + coût d'acquisition de volume -> scope all_inflation uniquement. + +fee_taxonomy: + exclude: + - funding_payment # zéro-somme entre traders + separate_components: + - spot_fee # structure distincte, ne jamais additionner les taux + - twap_fill_fee # userTwapSliceFills : format distinct, décoder séparément + +validation: + checks: + - id: af_conservation + kind: onchain_conservation + description: "Inflows USDC de 0xfefe…fefe ≈ part AF de Σ fills.fee" + tolerance_bps: 200 + severity: blocking + note: "Test fort et purement observationnel. Le meilleur dont on dispose ici." + - id: s3_vs_rest_parity + kind: cross_source + description: "Σ fills archive S3 == Σ fills API REST sur 24h" + tolerance: exact + severity: blocking + note: >- + IDENTIQUE À L'UNITÉ PRÈS. Toute divergence signale un bug de format ou de + fenêtrage — à corriger avant toute publication. + - id: hlp_equity + kind: cross_check + description: "Équité du HLP vault (clearinghouseState) vs part résiduelle" + tolerance_bps: 500 + +ui: + annotations: + - target: token_holder_value + text: >- + L'AF reçoit de l'USDC, achète du HYPE, puis le brûle. On comptabilise la valeur + REÇUE (certaine), pas la valeur du HYPE brûlé (qui dépend du prix d'exécution). + Les deux sont exposées ; le classement utilise la première. + +metadata: + chains: [hyperliquid] + is_router: false + nests_within: null + docs: https://hyperliquid.gitbook.io/hyperliquid-docs diff --git a/benchmarks/apps/raydium.yaml b/benchmarks/apps/raydium.yaml new file mode 100644 index 00000000..1247c6bd --- /dev/null +++ b/benchmarks/apps/raydium.yaml @@ -0,0 +1,154 @@ +schema_version: 1 +slug: raydium +name: Raydium +category: dex +methodology_version: 1 +status: live +verifiability: tier_1 + +# Piège dominant : accrual vs claim. +# Les fees s'accumulent dans les vaults et sont réclamés au seuil de $10. +# Mesurer les inflows du wallet de buyback mesure des CLAIMS décalés de plusieurs jours. +# SOURCE PRIMAIRE = accrual par swap depuis AmmConfig. Les inflows = témoin cumulatif. + +deployments: + - id: raydium:solana + chain: solana + effective_from: "2021-02-01T00:00:00Z" + + finality: + mode: solana_finalized + lag_seconds: 30 + + source: + kind: solana_rpc + canonical: true + rpc: helius # Helius ou Triton, clé en env + method: getSignaturesForAddress # + getTransaction par sig + note: >- + Décoder les comptes PoolState + AmmConfig par pool. + getSignaturesForAddress sur les programmes collecteurs, puis getTransaction. + + programs: + amm_v4: + id: "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" + fee_collector: "PNLCQcVCD26aC7ZWgRyr5ptfaR7bBrWdTFgRWwu2tvF" + clmm: + id: "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK" + fee_collector: "projjosVCPQH49d5em7VYS7fJZzaqKixqKtus7yk416" + cpmm: + id: "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C" + fee_collector: "ProCXqRcXJjoUd1RNoo28bSizAA6EEqt9wURZYPDc5u" + + destinations: + ray_buyback: "DdHDoz94o2WJmD9myRobHCwtx1bESpHTd4SSPe6VEZaz" + treasury_clmm: "FundHfY8oo8J9KYGyfXFFuQCHe7Z1VBNmsj84eMcdYs4" + treasury_cpmm: "FUNDduJTA7XcckKHKfAoEnnhuSud2JUCUZv6opWEjrBU" + + witnesses: + - kind: defillama + endpoint: https://api.llama.fi/summary/fees/raydium + - kind: onchain_buyback_wallet + address: "DdHDoz94o2WJmD9myRobHCwtx1bESpHTd4SSPe6VEZaz" + note: "Témoin cumulatif des claims (décalés). Pas la source primaire." + +allocation: + # Splits lus depuis AmmConfig on-chain (trade_fee_rate, protocol_fee_rate, fund_fee_rate). + # Dénominateur 10^6. Les plateformes tierces choisissent leur cpConfigId -> lire par pool. + model: amm_config_per_pool + splits_by_pool_type: + amm_v4: + lp: 0.88 + token_holder: 0.12 # buyback RAY -> DdHDoz94 + treasury: 0.00 + source_url: https://docs.raydium.io/raydium/protocol/developers/addresses + used_for: plausibility_check_only + clmm: + lp: 0.84 + token_holder: 0.12 + treasury: 0.04 + used_for: plausibility_check_only + cpmm: + lp: 0.84 + token_holder: 0.12 + treasury: 0.04 + used_for: plausibility_check_only + + rules: + - component: swap_fee + share_source: onchain_config + read: amm_config_per_pool + note: "Lire AmmConfig par pool. Ne jamais utiliser les pourcentages ci-dessus au calcul." + splits: + - beneficiary: lp + field: lp_rate + - beneficiary: token_holder + field: protocol_fee_rate + note: >- + CORRECTION AU BRIEF : le RAY est TRANSFÉRÉ vers DdHDoz94, pas brûlé par le + mécanisme. beneficiary = token_holder, pas burn. + - beneficiary: treasury + field: fund_fee_rate + + # Creator fee CPMM : additionnel au trade fee, pas un prélèvement dessus. + - component: bonding_curve_fee + source: cpmm_creator_fee + beneficiary: creator + share_source: onchain_field + note: "L'ignorer sous-estime gross_fees." + + accrual_note: >- + COMPTABILISER À L'ACCRUAL, PAR SWAP. Pas au claim. + Les fees s'accumulent dans les vaults jusqu'au seuil de $10, puis sont transférés + par batch. La latence claim->accrual peut atteindre plusieurs jours en période calme. + Les inflows du wallet buyback servent de témoin cumulatif, pas de source primaire. + +pricing: + primary: pyth_hermes + witness: chainlink + granularity: hourly_twap + fallback: pool_ratio + fallback_confidence: low + fallback_note: >- + Memecoins Solana majoritaires en volume. pool_ratio = ratio des réserves au moment + du swap, lu on-chain. Marqué confidence:low, astérisque UI. Ne jamais inventer un prix. + token2022_note: >- + Calculer sur le montant NET entrant dans le vault (après transfer fee Token-2022), + pas sur le montant signé. + +emissions: + scope: incentives_only + sources: + - kind: farm_distributions + token: RAY + note: "Émissions des farms Raydium vers les LPs. Lire les distributions effectives." + +edge_cases: + - id: clmm_fee_growth + description: >- + CLMM : fees dans le token d'entrée seulement. Suivre fee_growth_global_0/1_x64 + séparément. Pas d'accumulation symétrique. + - id: custom_amm_config + description: "Pools tiers à config custom : lire AmmConfig par pool, obligatoirement." + - id: token2022_transfer_fee + description: "Montant net entrant dans le vault, pas montant signé." + +validation: + checks: + - id: accrual_vs_claims + kind: cross_check + description: "Accruals calculés sur 30j vs claims observés (avec décalage) < 3%" + tolerance_bps: 300 + - id: buyback_balance + kind: onchain_conservation + description: "Balance cumulative de DdHDoz94 vs Σ buybacks calculés" + tolerance_bps: 500 + - id: witness_defillama + kind: witness + quarantine_above_bps: 500 + +metadata: + chains: [solana] + is_router: false + nests_within: null + docs: https://docs.raydium.io diff --git a/benchmarks/apps/uniswap.yaml b/benchmarks/apps/uniswap.yaml new file mode 100644 index 00000000..9da2e206 --- /dev/null +++ b/benchmarks/apps/uniswap.yaml @@ -0,0 +1,201 @@ +schema_version: 1 +slug: uniswap +name: Uniswap +category: dex +methodology_version: 1 +status: live +verifiability: tier_1 + +# LE PROTOCOLE LE PLUS PIÉGEUX DU SCOPE. +# Trois axes indépendants : version (v2/v3/v4) x chaîne (~11) x date d'activation. +# Un effective_from par COMBINAISON. Appliquer un taux sur une période où il valait +# zéro est le bug Lido inversé. + +deployments: + # ------------------------------------------------------------------------- + # Ethereum mainnet v2 — adresses VÉRIFIÉES + # ------------------------------------------------------------------------- + - id: uniswap:ethereum:v2 + chain: ethereum + version: v2 + # UNIfication exécutée on-chain le 27/12/2025. AVANT : zéro protocol revenue. + effective_from: "2025-12-27T20:33:00Z" + finality: { mode: beacon_finalized, lag_seconds: 800 } + + source: + kind: evm_logs + canonical: true + # Contrat immuable où s'accumulent les fees. VÉRIFIÉ (Uniswap/protocol-fees + Etherscan). + token_jar: "0xf38521f130fcCF29dB1961597bc5d2B60F995f85" + # Releaser : brûler de l'UNI pour retirer. Destinataire 0xdead. + firepit: "0x0D5Cd355e2aBEB8fb1552F56c965B867346d6721" + read: token_jar_inflows + # On compte les INFLOWS (accrual), pas les burns Firepit (claims). Les deux + # sont décalés : le jar se vide par arbitrage quand la valeur dépasse le seuil. + + witnesses: + - kind: subgraph + id: uniswap-v2-ethereum + - kind: defillama + endpoint: https://api.llama.fi/summary/fees/uniswap + note: "Divergence attendue sur les périodes pré-fee-switch." + + # ------------------------------------------------------------------------- + # Ethereum mainnet v3 + # ------------------------------------------------------------------------- + - id: uniswap:ethereum:v3 + chain: ethereum + version: v3 + effective_from: "2025-12-27T20:33:00Z" + source: + kind: evm_logs + canonical: true + token_jar: "0xf38521f130fcCF29dB1961597bc5d2B60F995f85" + v3_fee_adapter: "0x5E74C9f42EEd283bFf3744fBD1889d398d40867d" + v3_open_fee_adapter: "0xf2371551Fe3937Db7c750f4DfABe5c2fFFdcBf5A" + read: token_jar_inflows + note: >- + SEULE UNE SÉLECTION de pools v3 a été activée (80-95% des LP fees mainnet). + Lire l'état de l'adaptateur pool par pool. Ne pas supposer une activation globale. + + # ------------------------------------------------------------------------- + # Autres chaînes et v4 — À COMPLÉTER APRÈS VÉRIFICATION + # ------------------------------------------------------------------------- + - id: uniswap:arbitrum:v3 + chain: arbitrum + version: v3 + status: pending_verification + effective_from: "TODO" + source: + kind: evm_logs + token_jar: "TODO_une_adresse_par_chaine" + note: >- + Propositions 94/95 (mars 2026) ont étendu à Arbitrum, Base, OP Mainnet, Soneium, + X Layer, Worldchain, Zora, Celo. Propositions ultérieures (96, 100...) ont ajouté + BNB Chain, Polygon et l'activation v4. + ÉNUMÉRER depuis gov.uniswap.org et les transactions d'exécution. + Récupérer chaque TokenJar depuis github.com/Uniswap/protocol-fees et le VÉRIFIER + sur un explorer avant de l'inscrire ici. + + - id: uniswap:ethereum:v4 + chain: ethereum + version: v4 + status: pending_verification + effective_from: "TODO" + note: >- + Le V4FeeAdapter était marqué work-in-progress dans le repo. Confirmer son + déploiement et sa date d'activation avant d'activer ce déploiement. + +allocation: + # ATTENTION : le protocol fee est un PRÉLÈVEMENT sur les LP fees, PAS un supplément. + # Le brief d'origine affirmait l'inverse ; le traiter comme additif surestime + # gross_fees d'environ 1/6 sur tout Uniswap. + model: carve_out_from_lp_fees + + rules: + - version: v2 + # LP fees : 0.30% -> 0.25%. 0.05% du volume va au protocole. + splits: + - beneficiary: token_holder + component: swap_fee + share_source: constant + value: 0.16667 # 0.05 / 0.30 + effective_from: "2025-12-27T20:33:00Z" + effective_to: null + source_url: https://gov.uniswap.org/t/unification-proposal/25881 + note: >- + Les fee levels v2 sont hardcodés dans les contrats et la gouvernance les + active ou désactive globalement. `constant` est ici légitime, mais borné + dans le temps et sourcé — c'est ce qui manquait au cas Lido. + - beneficiary: lp + component: swap_fee + share_source: derived # gross - protocol_fee + verify_against: token_jar_inflows + + - version: v3 + # 25% des LP fees sur les tiers 0.01% et 0.05% ; 16.7% sur 0.30% et 1%. + # Ajustable pool par pool par gouvernance -> DOIT être lu on-chain. + splits: + - beneficiary: token_holder + component: swap_fee + share_source: onchain_config + contract: v3_fee_adapter + read: protocol_fee_per_pool + fallback_reference: + tier_0_01: 0.25 + tier_0_05: 0.25 + tier_0_30: 0.167 + tier_1_00: 0.167 + used_for: plausibility_check_only + - beneficiary: lp + component: swap_fee + share_source: derived + + # Fees prélevés par des hooks v4 tiers : PAS du revenue Uniswap. + third_party_rules: + - source: v4_hooks + beneficiary: third_party + component: interface_fee + is_app_layer: true + + exclusions: + - id: treasury_burn_100m_uni + date: "2025-12-27" + reason: >- + Burn rétroactif one-off de 100M UNI depuis le trésor (~$596M), destiné à + représenter ce qui aurait été brûlé si le fee switch avait été actif depuis 2018. + CE N'EST PAS DU REVENUE. L'inclure produirait un pic absurde qui discréditerait + tout le classement. + +non_trading_revenue: + - id: unichain_sequencer + component: sequencer_revenue + beneficiary: burn + counted_in: non_trading_holder_value # HORS gross_fees + formula: "sequencer_fees - l1_data_costs - optimism_share(15%)" + rationale: >- + Entre dans le même mécanisme de burn que les protocol fees, mais ce n'est pas un + frais de trading. Additionné séparément et VISIBLEMENT à part dans l'UI. + +pricing: + primary: pyth_hermes + witness: chainlink + granularity: hourly_twap + fallback: pool_ratio + fallback_confidence: low + +emissions: + scope: incentives_only + sources: [] + total: 0 + note: "Pas de liquidity mining. emissions_cost = 0." + +interface_fees: + status: disabled + since: "2025-12-27" + note: >- + Uniswap Labs a coupé ses frontend/wallet/API fees lors d'UNIfication. Les compter + chez les concurrents fausserait la comparaison — d'où excluded_from_ranking dans + la taxonomie partagée. + +validation: + checks: + - id: zero_before_activation + kind: unit_test + description: "protocol revenue == 0 avant effective_from, PAR CHAÎNE ET PAR VERSION" + severity: blocking + - id: token_jar_conservation + kind: onchain_conservation + description: "Inflows TokenJar ≈ Σ events de collecte de protocol fee" + tolerance_bps: 200 + - id: protocol_fee_ratio_v2 + kind: plausibility + description: "protocol_fee / gross_fees ≈ 1/6 sur v2" + alert_if_deviation_gt: 0.03 + note: "Un écart signale un déploiement manqué ou un effective_from erroné." + +metadata: + chains: [ethereum] # étendre après vérification + is_router: false + nests_within: null + repo: https://github.com/Uniswap/protocol-fees diff --git a/harnesses/apps/Dockerfile b/harnesses/apps/Dockerfile new file mode 100644 index 00000000..4058acc2 --- /dev/null +++ b/harnesses/apps/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.24-alpine AS builder +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o /app/collector ./cmd/collector +RUN CGO_ENABLED=0 go build -o /app/materializer ./cmd/materializer + +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates tzdata +COPY --from=builder /app/collector /app/collector +COPY --from=builder /app/materializer /app/materializer +COPY migrations/ /app/migrations/ diff --git a/harnesses/apps/cmd/collector/main.go b/harnesses/apps/cmd/collector/main.go new file mode 100644 index 00000000..26755dec --- /dev/null +++ b/harnesses/apps/cmd/collector/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "time" + + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/collect/rest" + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/ledger" + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +func main() { + ctx := context.Background() + + db, err := ledger.New(ctx, mustEnv("DATABASE_URL")) + if err != nil { + log.Fatalf("collector: %v", err) + } + defer db.Close() + + go func() { + http.Handle("/metrics", promhttp.Handler()) + http.ListenAndServe(":2112", nil) + }() + + dydx := rest.NewDyDX() + + for { + if err := runDyDX(ctx, db, dydx); err != nil { + log.Printf("dydx collector error: %v", err) + } + time.Sleep(60 * time.Second) + } +} + +func runDyDX(ctx context.Context, db *ledger.DB, col *rest.DyDXCollector) error { + const deploymentID = "dydx-v4:dydx-chain" + + from, err := db.GetCursor(ctx, deploymentID) + if err != nil { + return err + } + + to := spec.Cursor{ + Height: ^uint64(0), // collect up to tip + Ts: time.Now(), + Finalized: true, + } + + out := make(chan spec.FeeEvent, 1000) + var batch []spec.FeeEvent + + go func() { + defer close(out) + _, err = col.Collect(ctx, deploymentID, from, to, out) + }() + + for e := range out { + batch = append(batch, e) + if len(batch) >= 500 { + if err := db.UpsertEvents(ctx, batch); err != nil { + return err + } + batch = batch[:0] + } + } + if len(batch) > 0 { + if err := db.UpsertEvents(ctx, batch); err != nil { + return err + } + } + + return nil +} + +func mustEnv(key string) string { + v := os.Getenv(key) + if v == "" { + log.Fatalf("missing required env: %s", key) + } + return v +} diff --git a/harnesses/apps/cmd/materializer/main.go b/harnesses/apps/cmd/materializer/main.go new file mode 100644 index 00000000..f6134538 --- /dev/null +++ b/harnesses/apps/cmd/materializer/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "time" + + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/invariant" + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/ledger" +) + +func main() { + ctx := context.Background() + + db, err := ledger.New(ctx, mustEnv("DATABASE_URL")) + if err != nil { + log.Fatalf("materializer: %v", err) + } + defer db.Close() + + checker := invariant.New(db.Pool()) + + go func() { + http.Handle("/metrics", promhttp.Handler()) + http.ListenAndServe(":2112", nil) + }() + + for { + if err := runMaterialize(ctx, db, checker); err != nil { + log.Printf("materializer error: %v", err) + } + time.Sleep(5 * time.Minute) + } +} + +func runMaterialize(ctx context.Context, db *ledger.DB, checker *invariant.Checker) error { + const mv = 1 + deployments := []string{"dydx-v4:dydx-chain"} + + for _, dep := range deployments { + if err := db.Materialize(ctx, dep, mv); err != nil { + return err + } + if err := checker.CheckAll(ctx, dep, mv); err != nil { + log.Printf("INVARIANT FAILED %s: %v — quarantining", dep, err) + if qErr := db.Quarantine(ctx, dep, err.Error()); qErr != nil { + log.Printf("quarantine write failed: %v", qErr) + } + continue + } + if err := db.RefreshMaterializedView(ctx); err != nil { + return err + } + } + return nil +} + +func mustEnv(key string) string { + v := os.Getenv(key) + if v == "" { + log.Fatalf("missing required env: %s", key) + } + return v +} diff --git a/harnesses/apps/docker-compose.apps.yml b/harnesses/apps/docker-compose.apps.yml new file mode 100644 index 00000000..dc6136f0 --- /dev/null +++ b/harnesses/apps/docker-compose.apps.yml @@ -0,0 +1,65 @@ +# Fragment docker-compose à merger dans /opt/ocb/docker-compose.yml sur le VPS. +# Données sur /data (sdb, 200GB) — isolé du disque OS. + +services: + postgres: + image: postgres:16 + container_name: ocb-postgres + restart: unless-stopped + env_file: /run/ocb/.env.apps + volumes: + - /data/postgres:/var/lib/postgresql/data + networks: [web] + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ocb -d apps"] + interval: 30s + timeout: 5s + retries: 3 + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } + + postgres-exporter: + image: prometheuscommunity/postgres-exporter:latest + container_name: ocb-postgres-exporter + restart: unless-stopped + environment: + DATA_SOURCE_NAME: "postgresql://ocb:${POSTGRES_PASSWORD}@ocb-postgres:5432/apps?sslmode=disable" + networks: [web] + expose: ["9187"] + depends_on: + postgres: + condition: service_healthy + logging: + driver: json-file + options: { max-size: "5m", max-file: "2" } + + apps-collector: + build: /opt/ocb/harnesses/apps + container_name: ocb-apps-collector + restart: unless-stopped + env_file: /run/ocb/.env.apps + command: ["/app/collector"] + networks: [web] + expose: ["2112"] + depends_on: + postgres: + condition: service_healthy + logging: + driver: json-file + options: { max-size: "20m", max-file: "5" } + + apps-materializer: + build: /opt/ocb/harnesses/apps + container_name: ocb-apps-materializer + restart: unless-stopped + env_file: /run/ocb/.env.apps + command: ["/app/materializer"] + networks: [web] + expose: ["2112"] + depends_on: + postgres: + condition: service_healthy + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } diff --git a/harnesses/apps/env.apps.example b/harnesses/apps/env.apps.example new file mode 100644 index 00000000..dc248ed6 --- /dev/null +++ b/harnesses/apps/env.apps.example @@ -0,0 +1,32 @@ +# /run/ocb/.env.apps — chiffré avec SOPS avant deploy +# Générer avec : sops encrypt --filename-override secrets/.env.apps.enc env.apps.example + +POSTGRES_USER=ocb +POSTGRES_PASSWORD=CHANGEME +POSTGRES_DB=apps +DATABASE_URL=postgresql://ocb:CHANGEME@ocb-postgres:5432/apps?sslmode=disable + +# dYdX +DYDX_INDEXER_URL=https://indexer.dydx.trade/v4 +DYDX_NODE_GRPC=https://dydx-ops-rpc.kingnodes.com:443 + +# Hyperliquid +HL_API_URL=https://api.hyperliquid.xyz/info +HL_S3_BUCKET=hl-mainnet-node-data +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION=us-east-1 + +# GMX +ARBITRUM_RPC_URL= +AVALANCHE_RPC_URL= + +# Pyth Hermes +PYTH_HERMES_URL=https://hermes.pyth.network + +# Prometheus push (si push gateway) +PROM_PUSHGATEWAY_URL=http://ocb-prom:9091 + +# Alerting +QUARANTINE_GITHUB_TOKEN= +QUARANTINE_GITHUB_REPO=ChainBench/OpenChainBench diff --git a/harnesses/apps/go.mod b/harnesses/apps/go.mod new file mode 100644 index 00000000..8344b4e0 --- /dev/null +++ b/harnesses/apps/go.mod @@ -0,0 +1,26 @@ +module github.com/ChainBench/OpenChainBench/harnesses/apps + +go 1.24.0 + +require ( + github.com/jackc/pgx/v5 v5.7.2 + github.com/prometheus/client_golang v1.20.5 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) diff --git a/harnesses/apps/go.sum b/harnesses/apps/go.sum new file mode 100644 index 00000000..9b2736a7 --- /dev/null +++ b/harnesses/apps/go.sum @@ -0,0 +1,52 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/harnesses/apps/internal/collect/rest/dydx.go b/harnesses/apps/internal/collect/rest/dydx.go new file mode 100644 index 00000000..fe67f19e --- /dev/null +++ b/harnesses/apps/internal/collect/rest/dydx.go @@ -0,0 +1,199 @@ +package rest + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +const dydxIndexer = "https://indexer.dydx.trade/v4" + +type DyDXCollector struct { + client *http.Client +} + +func NewDyDX() *DyDXCollector { + return &DyDXCollector{client: &http.Client{Timeout: 30 * time.Second}} +} + +func (c *DyDXCollector) Name() string { return "dydx-rest" } + +type dydxFill struct { + ID string `json:"id"` + CreatedAt string `json:"createdAt"` + CreatedAtHeight string `json:"createdAtHeight"` + Side string `json:"side"` + Liquidity string `json:"liquidity"` // TAKER | MAKER + Type string `json:"type"` + Market string `json:"market"` + Price string `json:"price"` + Size string `json:"size"` + Fee string `json:"fee"` // USDC, 6 decimals, may be negative (rebate) + SubaccountID string `json:"subaccountId"` + AffiliateRevShare string `json:"affiliateRevShare"` // USDC, optional +} + +func (c *DyDXCollector) Collect( + ctx context.Context, + deploymentID string, + from, to spec.Cursor, + out chan<- spec.FeeEvent, +) (spec.Cursor, error) { + cursor := from + pageSize := 100 + + for { + fills, nextHeight, err := c.fetchFills(ctx, cursor.Height, pageSize) + if err != nil { + return cursor, fmt.Errorf("dydx: fetch fills at height %d: %w", cursor.Height, err) + } + if len(fills) == 0 { + break + } + + for _, f := range fills { + h, _ := strconv.ParseUint(f.CreatedAtHeight, 10, 64) + // [from, to) — exclude events at or above `to` + if h >= to.Height { + return cursor, nil + } + + ts, err := time.Parse(time.RFC3339Nano, f.CreatedAt) + if err != nil { + continue + } + + // Emit taker or maker fee event + component, beneficiary := classifyDyDXFill(f) + if component == "" { + continue + } + + feeAmt := f.Fee + isRebate := len(feeAmt) > 0 && feeAmt[0] == '-' + + if isRebate { + // Maker rebate: excluded from gross_fees, beneficiary = lp + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("dydx:%s:fee", f.ID), + Ts: ts, + Height: h, + Component: "maker_rebate", + Beneficiary: "lp", + Token: "USDC", + AmountRaw: trimNegative(feeAmt), + Decimals: 6, + Market: f.Market, + Finality: spec.FinalityFinal, + Source: "dydx-indexer", + } + } else { + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("dydx:%s:fee", f.ID), + Ts: ts, + Height: h, + Component: component, + Beneficiary: beneficiary, + Token: "USDC", + AmountRaw: toMicroUSDC(feeAmt), + Decimals: 6, + Market: f.Market, + Finality: spec.FinalityFinal, + Source: "dydx-indexer", + } + } + + // Affiliate rev share (third_party leg) + if f.AffiliateRevShare != "" && f.AffiliateRevShare != "0" { + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("dydx:%s:affiliate", f.ID), + Ts: ts, + Height: h, + Component: "affiliate_fee", + Beneficiary: "third_party", + Token: "USDC", + AmountRaw: toMicroUSDC(f.AffiliateRevShare), + Decimals: 6, + Market: f.Market, + Finality: spec.FinalityFinal, + Source: "dydx-indexer", + } + } + + cursor = spec.Cursor{Height: h, Ts: ts, Finalized: true} + } + + if nextHeight == 0 || nextHeight >= to.Height { + break + } + } + + return cursor, nil +} + +func (c *DyDXCollector) fetchFills(ctx context.Context, fromHeight uint64, limit int) ([]dydxFill, uint64, error) { + url := fmt.Sprintf("%s/fills?limit=%d&createdAtHeight=%d", dydxIndexer, limit, fromHeight) + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + resp, err := c.client.Do(req) + if err != nil { + return nil, 0, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, 0, fmt.Errorf("HTTP %d", resp.StatusCode) + } + + var result struct { + Fills []dydxFill `json:"fills"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, 0, err + } + + var maxHeight uint64 + for _, f := range result.Fills { + h, _ := strconv.ParseUint(f.CreatedAtHeight, 10, 64) + if h > maxHeight { + maxHeight = h + } + } + + return result.Fills, maxHeight, nil +} + +func classifyDyDXFill(f dydxFill) (component, beneficiary string) { + switch f.Liquidity { + case "TAKER": + return "taker_fee", "staker" // split handled by materializer via allocation_params + case "MAKER": + return "maker_fee", "lp" + } + return "", "" +} + +func toMicroUSDC(s string) string { + // Input is already in USDC with 6 decimal places from the indexer + // Strip the decimal point and return as integer string + // e.g. "1.234567" -> "1234567" + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return "0" + } + return strconv.FormatInt(int64(f*1e6), 10) +} + +func trimNegative(s string) string { + if len(s) > 0 && s[0] == '-' { + return s[1:] + } + return s +} diff --git a/harnesses/apps/internal/invariant/checks.go b/harnesses/apps/internal/invariant/checks.go new file mode 100644 index 00000000..732fdab4 --- /dev/null +++ b/harnesses/apps/internal/invariant/checks.go @@ -0,0 +1,130 @@ +package invariant + +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var failures = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "ocb_app_invariant_failures_total", + Help: "Number of accounting invariant failures by deployment and invariant name.", +}, []string{"deployment", "invariant"}) + +type Checker struct { + pool *pgxpool.Pool +} + +func New(pool *pgxpool.Pool) *Checker { + return &Checker{pool: pool} +} + +// CheckAll runs all invariants for a deployment after a materializer run. +// Returns the first error found, or nil if all pass. +func (c *Checker) CheckAll(ctx context.Context, deploymentID string, methodologyVersion int) error { + if err := c.checkI1GrossSum(ctx, deploymentID, methodologyVersion); err != nil { + failures.WithLabelValues(deploymentID, "I1").Inc() + return fmt.Errorf("I1 (gross sum): %w", err) + } + if err := c.checkI4Additivity(ctx, deploymentID, methodologyVersion); err != nil { + failures.WithLabelValues(deploymentID, "I4").Inc() + return fmt.Errorf("I4 (additivity): %w", err) + } + if err := c.checkI3NoNegatives(ctx, deploymentID); err != nil { + failures.WithLabelValues(deploymentID, "I3").Inc() + return fmt.Errorf("I3 (no negatives): %w", err) + } + return nil +} + +// I1: Σ(beneficiary amounts) == gross_fees — exact, not approximate. +// Any discrepancy reveals a decoder bug we want to see. +func (c *Checker) checkI1GrossSum(ctx context.Context, deploymentID string, mv int) error { + var unallocatedBPS float64 + err := c.pool.QueryRow(ctx, ` + WITH gross AS ( + SELECT SUM(amount_usd) AS total + FROM fee_facts + WHERE deployment_id = $1 + AND methodology_version = $2 + AND component NOT IN ('funding_payment','external_bribe','sequencer_revenue','maker_rebate') + ), + allocated AS ( + SELECT SUM(amount_usd) AS total + FROM fee_facts + WHERE deployment_id = $1 + AND methodology_version = $2 + ) + SELECT CASE WHEN gross.total > 0 + THEN ABS(gross.total - allocated.total) / gross.total * 10000 + ELSE 0 END + FROM gross, allocated`, + deploymentID, mv, + ).Scan(&unallocatedBPS) + if err != nil { + return err + } + if unallocatedBPS > 50 { + return fmt.Errorf("unallocated = %.1f bps (threshold 50 bps)", unallocatedBPS) + } + return nil +} + +// I4: sum of hourly buckets == sum of the full window — exact, not epsilon. +// Tests that [t0,t1) windows compose correctly. +func (c *Checker) checkI4Additivity(ctx context.Context, deploymentID string, mv int) error { + var diff float64 + err := c.pool.QueryRow(ctx, ` + WITH full_window AS ( + SELECT SUM(amount_usd) AS total + FROM fee_facts + WHERE deployment_id = $1 + AND methodology_version = $2 + AND bucket_start >= now() - INTERVAL '24 hours' + AND bucket_start < now() + ), + sum_of_halves AS ( + SELECT + SUM(CASE WHEN bucket_start < now() - INTERVAL '12 hours' THEN amount_usd ELSE 0 END) + + SUM(CASE WHEN bucket_start >= now() - INTERVAL '12 hours' THEN amount_usd ELSE 0 END) + AS total + FROM fee_facts + WHERE deployment_id = $1 + AND methodology_version = $2 + AND bucket_start >= now() - INTERVAL '24 hours' + AND bucket_start < now() + ) + SELECT ABS(COALESCE(full_window.total,0) - COALESCE(sum_of_halves.total,0)) + FROM full_window, sum_of_halves`, + deploymentID, mv, + ).Scan(&diff) + if err != nil { + return err + } + if diff != 0 { + return fmt.Errorf("additivity broken: diff = %f (must be exactly 0)", diff) + } + return nil +} + +// I3: no negative amounts except maker_rebate. +func (c *Checker) checkI3NoNegatives(ctx context.Context, deploymentID string) error { + var count int + err := c.pool.QueryRow(ctx, ` + SELECT COUNT(*) FROM fee_events + WHERE deployment_id = $1 + AND amount_raw < 0 + AND component != 'maker_rebate'`, + deploymentID, + ).Scan(&count) + if err != nil { + return err + } + if count > 0 { + return fmt.Errorf("%d events with negative amount_raw (non-rebate)", count) + } + return nil +} diff --git a/harnesses/apps/internal/ledger/ledger.go b/harnesses/apps/internal/ledger/ledger.go new file mode 100644 index 00000000..b8c3c825 --- /dev/null +++ b/harnesses/apps/internal/ledger/ledger.go @@ -0,0 +1,148 @@ +package ledger + +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +type DB struct { + pool *pgxpool.Pool +} + +func New(ctx context.Context, connStr string) (*DB, error) { + pool, err := pgxpool.New(ctx, connStr) + if err != nil { + return nil, fmt.Errorf("ledger: connect: %w", err) + } + return &DB{pool: pool}, nil +} + +func (db *DB) Close() { db.pool.Close() } + +// UpsertEvents inserts fee events idempotently. +// A duplicate (deployment_id, event_key, beneficiary) is silently ignored. +func (db *DB) UpsertEvents(ctx context.Context, events []spec.FeeEvent) error { + if len(events) == 0 { + return nil + } + + tx, err := db.pool.Begin(ctx) + if err != nil { + return fmt.Errorf("ledger: begin: %w", err) + } + defer tx.Rollback(ctx) + + for _, e := range events { + _, err := tx.Exec(ctx, ` + INSERT INTO fee_events + (deployment_id, event_key, beneficiary, ts, height, + component, token, amount_raw, decimals, market, + finality, source, meta) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8::numeric,$9,$10,$11,$12,$13) + ON CONFLICT (deployment_id, event_key, beneficiary) DO NOTHING`, + e.DeploymentID, e.EventKey, e.Beneficiary, e.Ts, e.Height, + e.Component, e.Token, e.AmountRaw, e.Decimals, e.Market, + string(e.Finality), e.Source, metaToJSON(e.Meta), + ) + if err != nil { + return fmt.Errorf("ledger: upsert event %s: %w", e.EventKey, err) + } + } + + return tx.Commit(ctx) +} + +// GetCursor returns the last known cursor for a deployment. +func (db *DB) GetCursor(ctx context.Context, deploymentID string) (spec.Cursor, error) { + var c spec.Cursor + err := db.pool.QueryRow(ctx, ` + SELECT height, COALESCE(block_hash,''), ts, finalized + FROM collector_cursors WHERE deployment_id = $1`, deploymentID, + ).Scan(&c.Height, &c.BlockHash, &c.Ts, &c.Finalized) + if err != nil { + return spec.Cursor{}, nil // no cursor yet = start from genesis + } + return c, nil +} + +// SaveCursor upserts the cursor after a successful batch. +func (db *DB) SaveCursor(ctx context.Context, deploymentID string, c spec.Cursor) error { + _, err := db.pool.Exec(ctx, ` + INSERT INTO collector_cursors (deployment_id, height, block_hash, ts, finalized, updated_at) + VALUES ($1,$2,$3,$4,$5,now()) + ON CONFLICT (deployment_id) DO UPDATE SET + height=EXCLUDED.height, block_hash=EXCLUDED.block_hash, + ts=EXCLUDED.ts, finalized=EXCLUDED.finalized, updated_at=now()`, + deploymentID, c.Height, c.BlockHash, c.Ts, c.Finalized, + ) + return err +} + +// Pool exposes the underlying pool for the invariant checker. +func (db *DB) Pool() *pgxpool.Pool { return db.pool } + +// Materialize computes fee_facts from fee_events for a deployment. +// Prices are stored as 1.0 for USDC (stable_passthrough); extend for non-stable tokens. +func (db *DB) Materialize(ctx context.Context, deploymentID string, mv int) error { + _, err := db.pool.Exec(ctx, ` + INSERT INTO fee_facts + (deployment_id, bucket_start, methodology_version, + component, beneficiary, token, + amount_native, price_usd, price_source, price_ts, price_confidence, + amount_usd, event_count, finality) + SELECT + deployment_id, + date_trunc('hour', ts) AS bucket_start, + $2 AS methodology_version, + component, + beneficiary, + token, + SUM(amount_raw::numeric / POW(10, decimals)) AS amount_native, + 1.0 AS price_usd, + 'stable_passthrough' AS price_source, + date_trunc('hour', ts) AS price_ts, + 'high' AS price_confidence, + SUM(amount_raw::numeric / POW(10, decimals)) AS amount_usd, + COUNT(*) AS event_count, + MIN(finality) AS finality + FROM fee_events + WHERE deployment_id = $1 + GROUP BY deployment_id, date_trunc('hour', ts), component, beneficiary, token + ON CONFLICT (deployment_id, bucket_start, methodology_version, component, beneficiary, token) + DO UPDATE SET + amount_native = EXCLUDED.amount_native, + amount_usd = EXCLUDED.amount_usd, + event_count = EXCLUDED.event_count, + finality = EXCLUDED.finality`, + deploymentID, mv, + ) + return err +} + +// Quarantine inserts a quarantine record for a deployment bucket. +func (db *DB) Quarantine(ctx context.Context, deploymentID, reason string) error { + _, err := db.pool.Exec(ctx, ` + INSERT INTO quarantine (deployment_id, bucket_start, reason, opened_at) + VALUES ($1, date_trunc('hour', now()), $2, now()) + ON CONFLICT (deployment_id, bucket_start, reason) DO NOTHING`, + deploymentID, reason, + ) + return err +} + +// RefreshMaterializedView refreshes fee_facts_windowed. +func (db *DB) RefreshMaterializedView(ctx context.Context) error { + _, err := db.pool.Exec(ctx, `REFRESH MATERIALIZED VIEW CONCURRENTLY fee_facts_windowed`) + return err +} + +func metaToJSON(m map[string]string) interface{} { + if len(m) == 0 { + return nil + } + return m +} diff --git a/harnesses/apps/internal/spec/types.go b/harnesses/apps/internal/spec/types.go new file mode 100644 index 00000000..53c5c64c --- /dev/null +++ b/harnesses/apps/internal/spec/types.go @@ -0,0 +1,52 @@ +package spec + +import "time" + +// HalfOpenRange enforces [From, To) semantics everywhere. +// No function in this codebase accepts two bare time.Time values for a window. +type HalfOpenRange struct { + From time.Time // inclusive + To time.Time // exclusive +} + +func (r HalfOpenRange) Contains(t time.Time) bool { + return !t.Before(r.From) && t.Before(r.To) +} + +type Finality string + +const ( + FinalityProvisional Finality = "provisional" + FinalityFinal Finality = "final" +) + +// FeeEvent is a raw fact in native units. No USD, no float. +type FeeEvent struct { + DeploymentID string + EventKey string // chain:tx_hash:log_index:sub — idempotence key + Ts time.Time + Height uint64 + Component string + Beneficiary string + Token string + AmountRaw string // NUMERIC(78,0) as string — never float64 + Decimals uint8 + Market string + Finality Finality + Source string + Meta map[string]string +} + +type Cursor struct { + Height uint64 + BlockHash string + Ts time.Time + Finalized bool +} + +// Collector is the only interface collectors implement. +// `to` is EXCLUDED — all windows are [from, to). +type Collector interface { + Name() string + Collect(ctx interface{}, deploymentID string, from, to Cursor, out chan<- FeeEvent) (Cursor, error) +} diff --git a/harnesses/apps/migrations/001_init.sql b/harnesses/apps/migrations/001_init.sql new file mode 100644 index 00000000..fbea9b6a --- /dev/null +++ b/harnesses/apps/migrations/001_init.sql @@ -0,0 +1,181 @@ +-- OpenChainBench /apps — ledger initial +-- Principe : fee_events est immuable et fait autorité. Tout le reste est dérivé +-- et reconstructible. Aucun UPDATE sur fee_events, uniquement des upserts idempotents. + +BEGIN; + +-- --------------------------------------------------------------------------- +-- Ledger brut. Unités natives, aucune conversion, aucun float. +-- --------------------------------------------------------------------------- +CREATE TABLE fee_events ( + deployment_id TEXT NOT NULL, + event_key TEXT NOT NULL, -- chain:tx_hash:log_index:sub + beneficiary TEXT NOT NULL, + ts TIMESTAMPTZ NOT NULL, -- timestamp du bloc, UTC + height BIGINT NOT NULL, + component TEXT NOT NULL, + token TEXT NOT NULL, + amount_raw NUMERIC(78,0) NOT NULL, -- entier natif. JAMAIS de float ici. + decimals SMALLINT NOT NULL, + market TEXT, + finality TEXT NOT NULL DEFAULT 'provisional', + source TEXT NOT NULL, + ingested_at TIMESTAMPTZ NOT NULL DEFAULT now(), + meta JSONB, + -- beneficiary fait partie de la clé : un même log émet souvent plusieurs + -- allocations (feeAmountForPool + feeReceiverAmount dans un PositionFeesCollected). + PRIMARY KEY (deployment_id, event_key, beneficiary), + CONSTRAINT finality_valid CHECK (finality IN ('provisional','final')) +); + +CREATE INDEX fee_events_ts ON fee_events (deployment_id, ts); +CREATE INDEX fee_events_height ON fee_events (deployment_id, height); +CREATE INDEX fee_events_provisional ON fee_events (deployment_id, height) + WHERE finality = 'provisional'; -- pour le rembobinage après reorg + +-- --------------------------------------------------------------------------- +-- Faits dérivés. Jetables : DELETE + recompute est une opération normale. +-- --------------------------------------------------------------------------- +CREATE TABLE fee_facts ( + deployment_id TEXT NOT NULL, + bucket_start TIMESTAMPTZ NOT NULL, -- bucket horaire, semi-ouvert [start, start+1h) + methodology_version INT NOT NULL, + component TEXT NOT NULL, + beneficiary TEXT NOT NULL, + token TEXT NOT NULL, + amount_native NUMERIC NOT NULL, + -- Le prix utilisé est stocké. Sans lui, un chiffre USD n'est pas reproductible + -- et donc pas auditable — c'est tout l'argument du produit. + price_usd NUMERIC NOT NULL, + price_source TEXT NOT NULL, + price_ts TIMESTAMPTZ NOT NULL, + price_confidence TEXT NOT NULL DEFAULT 'high', + amount_usd NUMERIC NOT NULL, + event_count INT NOT NULL, + finality TEXT NOT NULL, + PRIMARY KEY (deployment_id, bucket_start, methodology_version, + component, beneficiary, token), + CONSTRAINT price_confidence_valid CHECK (price_confidence IN ('high','medium','low')) +); + +CREATE INDEX fee_facts_bucket ON fee_facts (bucket_start, methodology_version); + +-- Témoins. Table séparée : jamais jointe aux faits canoniques, jamais sommée avec eux. +CREATE TABLE fee_facts_witness ( + deployment_id TEXT NOT NULL, + bucket_start TIMESTAMPTZ NOT NULL, + witness_source TEXT NOT NULL, + metric TEXT NOT NULL, + amount_usd NUMERIC NOT NULL, + fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (deployment_id, bucket_start, witness_source, metric) +); + +-- --------------------------------------------------------------------------- +-- Émissions. tokens_emitted est brut et séparé du prix : n'importe qui doit +-- pouvoir re-pricer avec sa propre source. +-- --------------------------------------------------------------------------- +CREATE TABLE emissions_daily ( + deployment_id TEXT NOT NULL, + day DATE NOT NULL, + token TEXT NOT NULL, + scope TEXT NOT NULL, -- incentives_only | all_inflation + tokens_emitted NUMERIC NOT NULL, + price_usd NUMERIC NOT NULL, + price_min_usd NUMERIC, -- bande d'incertitude si volatilité > 15% + price_max_usd NUMERIC, + price_method TEXT NOT NULL DEFAULT 'daily_twap', + cost_usd NUMERIC NOT NULL, + PRIMARY KEY (deployment_id, day, token, scope) +); + +-- --------------------------------------------------------------------------- +-- Paramètres d'allocation lus on-chain et versionnés dans le temps. +-- C'est la table qui empêche le bug Lido : aucun taux n'est appliqué en dehors +-- de son intervalle de validité observé. +-- --------------------------------------------------------------------------- +CREATE TABLE allocation_params ( + deployment_id TEXT NOT NULL, + param TEXT NOT NULL, + value NUMERIC NOT NULL, + valid_from TIMESTAMPTZ NOT NULL, + valid_to TIMESTAMPTZ, + observed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + source TEXT NOT NULL, -- contrat/méthode lue, ou URL de gouvernance + PRIMARY KEY (deployment_id, param, valid_from) +); + +-- Interdit deux valeurs simultanées pour un même paramètre. +CREATE EXTENSION IF NOT EXISTS btree_gist; +ALTER TABLE allocation_params + ADD CONSTRAINT allocation_params_no_overlap + EXCLUDE USING gist ( + deployment_id WITH =, + param WITH =, + tstzrange(valid_from, COALESCE(valid_to, 'infinity'::timestamptz)) WITH && + ); + +-- --------------------------------------------------------------------------- +-- Curseurs et audit +-- --------------------------------------------------------------------------- +CREATE TABLE collector_cursors ( + deployment_id TEXT PRIMARY KEY, + height BIGINT NOT NULL, + block_hash TEXT, -- détection de reorg + ts TIMESTAMPTZ NOT NULL, + finalized BOOLEAN NOT NULL DEFAULT false, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE methodology_versions ( + slug TEXT NOT NULL, + version INT NOT NULL, + applied_at TIMESTAMPTZ NOT NULL DEFAULT now(), + summary TEXT NOT NULL, + diff_json JSONB NOT NULL, -- alimente /apps/changelog + PRIMARY KEY (slug, version) +); + +CREATE TABLE quarantine ( + deployment_id TEXT NOT NULL, + bucket_start TIMESTAMPTZ NOT NULL, + reason TEXT NOT NULL, + detail JSONB, + issue_url TEXT, + opened_at TIMESTAMPTZ NOT NULL DEFAULT now(), + resolved_at TIMESTAMPTZ, + PRIMARY KEY (deployment_id, bucket_start, reason) +); + +-- --------------------------------------------------------------------------- +-- Vue de service du leaderboard. ~40 lignes pour le MVP. +-- Rafraîchie à chaque run du materializer ; la page ne calcule rien. +-- --------------------------------------------------------------------------- +CREATE MATERIALIZED VIEW fee_facts_windowed AS +SELECT + f.deployment_id, + w.window_label, + f.methodology_version, + SUM(f.amount_usd) FILTER (WHERE f.component NOT IN + ('funding_payment','external_bribe','sequencer_revenue','maker_rebate')) AS gross_fees_usd, + SUM(f.amount_usd) FILTER (WHERE f.beneficiary = 'lp') AS lp_revenue_usd, + SUM(f.amount_usd) FILTER (WHERE f.beneficiary IN ('treasury','insurance_fund')) AS protocol_cut_usd, + SUM(f.amount_usd) FILTER (WHERE f.beneficiary IN ('token_holder','staker','burn')) AS token_holder_value_usd, + MIN(f.finality) AS finality, + SUM(f.event_count) AS event_count +FROM fee_facts f +CROSS JOIN LATERAL ( + VALUES ('24h', INTERVAL '24 hours'), + ('7d', INTERVAL '7 days'), + ('30d', INTERVAL '30 days') +) AS w(window_label, span) +-- Fenêtre SEMI-OUVERTE. Le `<` de droite est le correctif du bug de +-- double-comptage à la seconde frontière (DefiLlama issue #8656). +WHERE f.bucket_start >= date_trunc('hour', now()) - w.span + AND f.bucket_start < date_trunc('hour', now()) +GROUP BY f.deployment_id, w.window_label, f.methodology_version; + +CREATE UNIQUE INDEX fee_facts_windowed_pk + ON fee_facts_windowed (deployment_id, window_label, methodology_version); + +COMMIT; diff --git a/harnesses/apps/prom-rules/alert_rules_apps.yml b/harnesses/apps/prom-rules/alert_rules_apps.yml new file mode 100644 index 00000000..34e0b4f0 --- /dev/null +++ b/harnesses/apps/prom-rules/alert_rules_apps.yml @@ -0,0 +1,151 @@ +groups: + - name: apps_infrastructure + rules: + - alert: AppsPostgresDown + expr: pg_up{job="postgres-exporter"} == 0 + for: 2m + labels: + severity: critical + team: ocb + annotations: + summary: "Postgres OCB-Apps est down" + description: "pg_up == 0 depuis 2 minutes. Collectors et materializer sont bloqués." + + - alert: AppsDataDiskWarning + expr: (node_filesystem_avail_bytes{mountpoint="/data"} / node_filesystem_size_bytes{mountpoint="/data"}) < 0.20 + for: 5m + labels: + severity: warning + team: ocb + annotations: + summary: "/data à plus de 80% de capacité" + description: "Disponible : {{ $value | humanize }}B. Prévoir extension ou nettoyage." + + - alert: AppsDataDiskCritical + expr: (node_filesystem_avail_bytes{mountpoint="/data"} / node_filesystem_size_bytes{mountpoint="/data"}) < 0.10 + for: 2m + labels: + severity: critical + team: ocb + annotations: + summary: "/data à plus de 90% — Postgres risque de crasher" + description: "Action immédiate requise." + + - alert: AppsPostgresConnSaturation + expr: pg_stat_database_numbackends{datname="apps"} / pg_settings_max_connections > 0.80 + for: 5m + labels: + severity: warning + team: ocb + annotations: + summary: "Postgres : connexions > 80% de max_connections" + + - alert: AppsDBSizeGrowthAnomaly + expr: deriv(pg_database_size_bytes{datname="apps"}[1h]) > 524288000 + for: 10m + labels: + severity: warning + team: ocb + annotations: + summary: "Postgres apps croît de >500MB/h" + description: "Probable bug de replay. Vérifier l'idempotence du collector." + + - name: apps_collectors + rules: + - alert: AppsCollectorLagWarning + expr: time() - ocb_app_data_freshness_seconds > 600 + for: 5m + labels: + severity: warning + team: ocb + annotations: + summary: "Collector /apps en retard > 10min" + description: "Deployment {{ $labels.deployment }} — dernier event il y a {{ $value | humanizeDuration }}." + + - alert: AppsCollectorDead + expr: time() - ocb_app_data_freshness_seconds > 3600 + for: 2m + labels: + severity: critical + team: ocb + annotations: + summary: "Collector /apps mort (> 1h sans données)" + description: "Deployment {{ $labels.deployment }} silencieux depuis {{ $value | humanizeDuration }}." + + - alert: AppsCollectorErrors + expr: rate(ocb_app_collector_errors_total[5m]) > 0.1 + for: 10m + labels: + severity: warning + team: ocb + annotations: + summary: "Erreurs collector /apps" + description: "{{ $labels.deployment }} / {{ $labels.kind }} : {{ $value | humanize }} err/s" + + - alert: AppsMaterializerStale + expr: time() - ocb_app_materializer_last_run_seconds > 1800 + for: 2m + labels: + severity: warning + team: ocb + annotations: + summary: "Materializer /apps n'a pas tourné depuis > 30min" + + - name: apps_data_quality + rules: + - alert: AppsInvariantFailed + expr: ocb_app_invariant_failures_total > 0 + for: 0m + labels: + severity: critical + team: ocb + annotations: + summary: "Invariant comptable cassé sur /apps" + description: >- + {{ $labels.deployment }} — invariant {{ $labels.invariant }}. + I1 (Σ bénéficiaires ≠ gross) ou I4 (additivité sous-fenêtres cassée). + Le chiffre NE DOIT PAS être publié. Quarantaine automatique en cours. + + - alert: AppsQuarantineOpen + expr: ocb_app_quarantined > 0 + for: 24h + labels: + severity: warning + team: ocb + annotations: + summary: "Ligne /apps en quarantaine non résolue > 24h" + description: "Deployment {{ $labels.deployment }} — voir issue GitHub liée." + + - alert: AppsWitnessDivergenceHigh + expr: ocb_app_witness_divergence_ratio > 0.05 + for: 30m + labels: + severity: warning + team: ocb + annotations: + summary: "Divergence témoin > 5% sur /apps" + description: >- + {{ $labels.deployment }} vs {{ $labels.witness }} : {{ $value | humanizePercentage }}. + Déclenchement automatique de la quarantaine si persistant. + + - alert: AppsUnallocatedFeeHigh + expr: ocb_app_unallocated_bps > 50 + for: 10m + labels: + severity: warning + team: ocb + annotations: + summary: "/apps : fees non alloués > 50 bps" + description: "{{ $labels.deployment }} — {{ $value }} bps non alloués. Bug de décodage probable." + + - alert: AppsZeroEventsOnActiveDeployment + expr: rate(ocb_app_collector_events_total[1h]) == 0 + for: 2h + labels: + severity: warning + team: ocb + annotations: + summary: "Zéro event collecté sur un déploiement actif" + description: >- + {{ $labels.deployment }} : aucun event depuis 2h. + Vérifier que les topic0 / hashes sont calculés au runtime et non zéro. From 32833ae7a2b08fc59636c5220e23bc5d5c686bf0 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:52:09 +0200 Subject: [PATCH 11/39] fix: interface ctx type, float-free toMicroUSDC, goroutine error capture, pagination cursor --- harnesses/apps/cmd/collector/main.go | 12 ++++-- harnesses/apps/internal/collect/rest/dydx.go | 40 ++++++++++++++++---- harnesses/apps/internal/spec/types.go | 7 +++- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/harnesses/apps/cmd/collector/main.go b/harnesses/apps/cmd/collector/main.go index 26755dec..37b177f4 100644 --- a/harnesses/apps/cmd/collector/main.go +++ b/harnesses/apps/cmd/collector/main.go @@ -53,13 +53,19 @@ func runDyDX(ctx context.Context, db *ledger.DB, col *rest.DyDXCollector) error } out := make(chan spec.FeeEvent, 1000) - var batch []spec.FeeEvent + collectErr := make(chan error, 1) go func() { defer close(out) - _, err = col.Collect(ctx, deploymentID, from, to, out) + newCursor, err := col.Collect(ctx, deploymentID, from, to, out) + if err != nil { + collectErr <- err + return + } + collectErr <- db.SaveCursor(ctx, deploymentID, newCursor) }() + var batch []spec.FeeEvent for e := range out { batch = append(batch, e) if len(batch) >= 500 { @@ -75,7 +81,7 @@ func runDyDX(ctx context.Context, db *ledger.DB, col *rest.DyDXCollector) error } } - return nil + return <-collectErr } func mustEnv(key string) string { diff --git a/harnesses/apps/internal/collect/rest/dydx.go b/harnesses/apps/internal/collect/rest/dydx.go index fe67f19e..6b6c9f32 100644 --- a/harnesses/apps/internal/collect/rest/dydx.go +++ b/harnesses/apps/internal/collect/rest/dydx.go @@ -131,9 +131,12 @@ func (c *DyDXCollector) Collect( cursor = spec.Cursor{Height: h, Ts: ts, Finalized: true} } - if nextHeight == 0 || nextHeight >= to.Height { + // Advance cursor past the max height seen to avoid re-fetching the same page. + // nextHeight is the highest block in this page; next call starts at nextHeight+1. + if nextHeight == 0 || nextHeight+1 >= to.Height { break } + cursor.Height = nextHeight + 1 } return cursor, nil @@ -180,15 +183,38 @@ func classifyDyDXFill(f dydxFill) (component, beneficiary string) { return "", "" } +// toMicroUSDC converts a decimal USDC string (e.g. "1.234567") to integer micro-USDC +// without using float64 to avoid rounding errors in accounting code. +// dYdX indexer always returns exactly 6 decimal places. func toMicroUSDC(s string) string { - // Input is already in USDC with 6 decimal places from the indexer - // Strip the decimal point and return as integer string - // e.g. "1.234567" -> "1234567" - f, err := strconv.ParseFloat(s, 64) - if err != nil { + if s == "" { return "0" } - return strconv.FormatInt(int64(f*1e6), 10) + dot := -1 + for i, c := range s { + if c == '.' { + dot = i + break + } + } + if dot == -1 { + // No decimal point — multiply by 1e6 + return s + "000000" + } + intPart := s[:dot] + fracPart := s[dot+1:] + // Pad or truncate to exactly 6 decimal places + for len(fracPart) < 6 { + fracPart += "0" + } + fracPart = fracPart[:6] + // Remove leading zeros from intPart to avoid octal interpretation, then combine + result := intPart + fracPart + // Strip leading zeros (but keep at least one digit) + for len(result) > 1 && result[0] == '0' { + result = result[1:] + } + return result } func trimNegative(s string) string { diff --git a/harnesses/apps/internal/spec/types.go b/harnesses/apps/internal/spec/types.go index 53c5c64c..9177ee7d 100644 --- a/harnesses/apps/internal/spec/types.go +++ b/harnesses/apps/internal/spec/types.go @@ -1,6 +1,9 @@ package spec -import "time" +import ( + "context" + "time" +) // HalfOpenRange enforces [From, To) semantics everywhere. // No function in this codebase accepts two bare time.Time values for a window. @@ -48,5 +51,5 @@ type Cursor struct { // `to` is EXCLUDED — all windows are [from, to). type Collector interface { Name() string - Collect(ctx interface{}, deploymentID string, from, to Cursor, out chan<- FeeEvent) (Cursor, error) + Collect(ctx context.Context, deploymentID string, from, to Cursor, out chan<- FeeEvent) (Cursor, error) } From 8b37bfe5b5afaba22107dfa4a5644c51763c3580 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:52:32 +0200 Subject: [PATCH 12/39] feat: /apps leaderboard foundation (#1797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add /apps leaderboard foundation — specs, Go harness, Postgres schema, alerting * fix: interface ctx type, float-free toMicroUSDC, goroutine error capture, pagination cursor --- benchmarks/apps/04-addendum.md | 112 +++++++++ benchmarks/apps/_taxonomy.yaml | 111 +++++++++ benchmarks/apps/aerodrome.yaml | 132 ++++++++++ benchmarks/apps/dydx-v4.yaml | 144 +++++++++++ benchmarks/apps/gmx-v2.yaml | 148 ++++++++++++ benchmarks/apps/hyperliquid.yaml | 175 ++++++++++++++ benchmarks/apps/raydium.yaml | 154 ++++++++++++ benchmarks/apps/uniswap.yaml | 201 ++++++++++++++++ harnesses/apps/Dockerfile | 13 + harnesses/apps/cmd/collector/main.go | 93 ++++++++ harnesses/apps/cmd/materializer/main.go | 68 ++++++ harnesses/apps/docker-compose.apps.yml | 65 +++++ harnesses/apps/env.apps.example | 32 +++ harnesses/apps/go.mod | 26 ++ harnesses/apps/go.sum | 52 ++++ harnesses/apps/internal/collect/rest/dydx.go | 225 ++++++++++++++++++ harnesses/apps/internal/invariant/checks.go | 130 ++++++++++ harnesses/apps/internal/ledger/ledger.go | 148 ++++++++++++ harnesses/apps/internal/spec/types.go | 55 +++++ harnesses/apps/migrations/001_init.sql | 181 ++++++++++++++ .../apps/prom-rules/alert_rules_apps.yml | 151 ++++++++++++ 21 files changed, 2416 insertions(+) create mode 100644 benchmarks/apps/04-addendum.md create mode 100644 benchmarks/apps/_taxonomy.yaml create mode 100644 benchmarks/apps/aerodrome.yaml create mode 100644 benchmarks/apps/dydx-v4.yaml create mode 100644 benchmarks/apps/gmx-v2.yaml create mode 100644 benchmarks/apps/hyperliquid.yaml create mode 100644 benchmarks/apps/raydium.yaml create mode 100644 benchmarks/apps/uniswap.yaml create mode 100644 harnesses/apps/Dockerfile create mode 100644 harnesses/apps/cmd/collector/main.go create mode 100644 harnesses/apps/cmd/materializer/main.go create mode 100644 harnesses/apps/docker-compose.apps.yml create mode 100644 harnesses/apps/env.apps.example create mode 100644 harnesses/apps/go.mod create mode 100644 harnesses/apps/go.sum create mode 100644 harnesses/apps/internal/collect/rest/dydx.go create mode 100644 harnesses/apps/internal/invariant/checks.go create mode 100644 harnesses/apps/internal/ledger/ledger.go create mode 100644 harnesses/apps/internal/spec/types.go create mode 100644 harnesses/apps/migrations/001_init.sql create mode 100644 harnesses/apps/prom-rules/alert_rules_apps.yml diff --git a/benchmarks/apps/04-addendum.md b/benchmarks/apps/04-addendum.md new file mode 100644 index 00000000..9ac40f78 --- /dev/null +++ b/benchmarks/apps/04-addendum.md @@ -0,0 +1,112 @@ +# `/apps` — Addendum aux décisions ouvertes + +**Date : 9 août 2026.** Ce document tranche les trois points laissés ouverts dans +`00-EXECUTION-BRIEF.md` et corrige un passage approximatif sur le budget de recompute. + +--- + +## Décision 1 — Renommage de la colonne de classement + +`net_real_revenue` est un abus de langage dès qu'on y inclut des buybacks : ce n'est pas +du revenue au sens comptable. Le terme induit le même péché de fusion que DefiLlama. + +**Décision** : la colonne de classement s'appelle `net_value_capture_usd`, libellé UI +**« Valeur nette captée »**. La formule est affichée **en permanence** sous l'en-tête de +colonne (pas au survol) : + +``` +Valeur nette captée += trésor + holders + burn − émissions +``` + +La barre segmentée décompose visuellement les trois termes positifs. Le chiffre scalaire +reste disponible pour le tri mais n'est jamais présenté sans sa formule. + +**Cohérence avec P4** : `with_holders` est le défaut. `treasury_only` est disponible en +toggle discret. Les deux variantes sont calculées et stockées dans `fee_facts_windowed` +sous `variant IN ('with_holders', 'treasury_only')`. + +--- + +## Décision 2 — Budget de recompute : critère chiffré + +La formulation « c'est un simple GROUP BY, ça tient en 30 minutes » était paresseuse. Le +vrai goulot d'étranglement est la jointure de pricing sur `(token, hour)`. + +**Analyse** : + +- Seules les paires `(token, heure)` où un event a eu lieu sont pricées : la table de + prix est **creuse et pilotée par la demande**, pas dense. +- Les prix sont une observation (pas un artefact de méthodologie) : un recompute de + formule ne les invalide pas. Seul un changement de méthode de pricing le fait, et c'est + rare. +- Le long tail Solana (memecoins) tombe en `pool_ratio`, dérivé du swap lui-même. Ce sont + les cas les moins chers : aucun appel externe. Seuls ~50 tokens majeurs touchent Pyth. + +**Critère d'acceptation chiffré (Phase 1)** : + +``` +Benchmark sur 20M d'events synthétiques, 3 000 tokens distincts : +- Recompute complet < 30 minutes sur le VPS (4 cores, 15GB RAM) +- EXPLAIN ANALYZE ne montre aucun Seq Scan sur fee_events ou fee_facts +- Si ces deux conditions ne sont pas satisfaites simultanément : + partitionner fee_events par mois (PARTITION BY RANGE sur ts) + et fee_facts par (bucket_start, methodology_version) +``` + +Le partitionnement n'est pas activé par défaut : il complique le schéma sans bénéfice +prouvé à l'échelle MVP. Le benchmark tranche, pas le raisonnement. + +--- + +## Décision 3 — Aerodrome en T1, phase 4a + +**Constat** : indexer les swaps Aerodrome en direct getLogs est intenable (milliers de +pools × millions de swaps). Mais on n'a pas besoin des swaps. + +Les fees d'Aerodrome atterrissent dans les contrats `FeesVotingReward` (un par gauge), +via `notifyRewardAmount()` appelé par le Voter. L'event émis est : + +```solidity +// IReward.sol — vérifié sur aerodrome-finance/contracts main +event NotifyReward( + address indexed from, // Voter + address indexed reward, // token de fee (USDC, WETH, etc.) + uint256 indexed epoch, // epochStart en secondes + uint256 amount // montant en unités natives +); + +// topic0 vérifié : +// 0x52977ea98a2220a03ee9ba5cb003ada08d394ea10155483c95dc2dc77a7eb24b +``` + +Volume réel : O(epoch × gauges actives × tokens) — quelques centaines d'events par +semaine, pas un par swap. **Aerodrome est indexable en T1 sans Envio.** + +**Prix à payer documenté** : granularité epoch (hebdomadaire). La fenêtre 24h d'Aerodrome +est de résolution dégradée — l'émission AERO est répartie uniformément sur 7 jours +(cf. `aerodrome.yaml : spread_over_epoch: true`) mais les fees sont discrets. L'UI affiche +un badge « résolution hebdomadaire » sur la ligne Aerodrome pour les fenêtres 24h et 7j. +La fenêtre 30j est pleine résolution. + +**Comment découvrir les FeesVotingReward** : via `Voter.gaugeToFees(gaugeAddress)`. +Énumérer les gauges actifs depuis `Voter`, résoudre leur contrat de fees à l'init, +surveiller les nouveaux gauges à chaque epoch. + +**Aerodrome passe de Phase 4 à Phase 4a** (parallèle à Uniswap, avant Raydium qui reste +le plus complexe). + +--- + +## Correction — passage approximatif dans 01-architecture.md + +Le §7 (séquencement) listait Raydium avant Uniswap. Ordre corrigé : + +| Phase | Contenu | +|---|---| +| 1 | Socle + dYdX | +| 2 | Hyperliquid + GMX V2 | +| 3 | Page `/apps` + API publique (3 protocoles) | +| 4a | Aerodrome (T1 via NotifyReward) + Uniswap | +| 4b | Raydium (le plus complexe : accrual Solana) | +| 5 | Changelog, exports CSV, coverage page | diff --git a/benchmarks/apps/_taxonomy.yaml b/benchmarks/apps/_taxonomy.yaml new file mode 100644 index 00000000..6aa2887f --- /dev/null +++ b/benchmarks/apps/_taxonomy.yaml @@ -0,0 +1,111 @@ +# Taxonomie partagée par toutes les specs /apps. +# Un fee_event ne peut porter qu'un `component` et qu'un `beneficiary` de ces listes. +# Toute modification ici est un changement de méthodologie : bump obligatoire de +# methodology_version sur les specs affectées + recompute complet. + +schema_version: 1 + +components: + taker_fee: + included_in_gross: true + label: "Frais preneur" + maker_fee: + included_in_gross: true + label: "Frais offreur" + maker_rebate: + included_in_gross: false + label: "Rabais offreur" + note: "Montant stocké positif, beneficiary=lp. Sortie de trésorerie, pas un fee." + swap_fee: + included_in_gross: true + label: "Frais de swap AMM" + borrow_fee: + included_in_gross: true + label: "Frais d'emprunt" + note: "Payé au pool, pas à une contrepartie. Revenue réel du système." + liquidation_fee: + included_in_gross: true + label: "Frais de liquidation" + note: "Uniquement la part retenue par le protocole ou le pool. Le collatéral saisi est une perte du trader, jamais un fee." + pool_creation_fee: + included_in_gross: true + label: "Frais de création de pool" + bonding_curve_fee: + included_in_gross: true + label: "Frais de bonding curve" + + # --- Collectés mais exclus des agrégats par défaut --- + funding_payment: + included_in_gross: false + toggleable: true + label: "Paiements de funding" + rationale: >- + Transfert de somme nulle entre longs et shorts. Le protocole n'en retient rien. + L'inclure gonfle mécaniquement les perps face aux DEX spot et rend le classement + inter-catégories inutilisable. + interface_fee: + included_in_gross: true + beneficiary_forced: third_party + is_app_layer: true + excluded_from_ranking: true + rationale: >- + Réel, mais Uniswap Labs a coupé ses frontend fees lors d'UNIfication. Les compter + chez les autres fausserait la comparaison. Mesuré, affiché, hors classement. + external_bribe: + included_in_gross: false + label: "Incentives externes" + rationale: "Payé par des tiers aux voters, pas un revenue généré par le protocole." + sequencer_revenue: + included_in_gross: false + counted_in: non_trading_holder_value + rationale: >- + Cas Unichain : entre dans le même mécanisme de burn que les protocol fees, mais + n'est pas un frais de trading. Additionné séparément et visiblement. + +exclusions: + # Jamais collectés. Documentés pour que la page méthodologie soit complète. + - id: price_impact + reason: "Capté par l'inventaire du LP, non identifiable comme fee dans les events." + - id: mev + reason: "Va au searcher et au validateur, pas au protocole." + - id: gas_and_priority_fees + reason: "Coût réseau, pas revenue protocole." + - id: seized_collateral + reason: "Perte du trader, pas un frais payé au système." + - id: treasury_one_offs + reason: >- + Burns rétroactifs et distributions exceptionnelles (ex. les 100M UNI du trésor + lors d'UNIfication). Ce n'est pas du revenue récurrent ; l'inclure produirait des + pics absurdes. + +beneficiaries: + lp: { label: "Fournisseurs de liquidité", in_protocol_cut: false, in_holder_value: false } + token_holder: { label: "Détenteurs du token", in_protocol_cut: false, in_holder_value: true } + staker: { label: "Stakers / validateurs", in_protocol_cut: false, in_holder_value: true } + burn: { label: "Brûlé", in_protocol_cut: false, in_holder_value: true } + treasury: { label: "Trésor", in_protocol_cut: true, in_holder_value: false } + insurance_fund: { label: "Fonds d'assurance", in_protocol_cut: true, in_holder_value: false } + third_party: { label: "Tiers (frontends, affiliés)", in_protocol_cut: false, in_holder_value: false } + creator: { label: "Créateur du pool / token", in_protocol_cut: false, in_holder_value: false } + +# Métriques dérivées. Ce sont des VUES sur la partition ci-dessus, pas des colonnes +# indépendantes. Aucune ne peut compter deux fois le même event. +derived: + gross_fees: "Σ components[included_in_gross]" + lp_revenue: "Σ beneficiary == lp" + protocol_cut: "Σ beneficiary.in_protocol_cut" + token_holder_value: "Σ beneficiary.in_holder_value" + emissions_cost: "Σ_jours tokens_émis(j) × TWAP_usd(j)" + net_real_revenue: + with_holders: "protocol_cut + token_holder_value + non_trading_holder_value − emissions_cost" + treasury_only: "protocol_cut − emissions_cost" + default: with_holders + note: >- + `treasury_only` correspond à la définition littérale du brief mais classe Hyperliquid + comme quasi nul, ce qui contredit l'intention. `with_holders` produit le classement + décrit. Les deux sont calculées ; à trancher avant le lancement public. + +invariants: + unallocated_tolerance_bps: 50 + witness_divergence_warn_bps: 200 + witness_divergence_quarantine_bps: 500 diff --git a/benchmarks/apps/aerodrome.yaml b/benchmarks/apps/aerodrome.yaml new file mode 100644 index 00000000..2f6c9e3f --- /dev/null +++ b/benchmarks/apps/aerodrome.yaml @@ -0,0 +1,132 @@ +schema_version: 1 +slug: aerodrome +name: Aerodrome +category: dex +methodology_version: 1 +status: live +verifiability: tier_2 + +# Le protocole qui justifie la colonne net_real_revenue. +# Résultat attendu : NÉGATIF. Ce n'est pas une anomalie à corriger. + +deployments: + - id: aerodrome:base + chain: base + effective_from: "2023-08-28T00:00:00Z" + + finality: + mode: l1_confirmed + lag_seconds: 900 + + source: + kind: envio_hyperindex + canonical: true + repo: https://github.com/velodrome-finance/indexer + note: >- + T2 : dépendance à un indexer tiers. Migration vers evm_logs directs prévue + en phase 5. L'architecture ve(3,3) rend cette dépendance structurellement + risquée à long terme. + + witnesses: + - kind: defillama + endpoint: https://api.llama.fi/summary/fees/aerodrome + expected_divergence: high + note: >- + DefiLlama ne soustrait pas les émissions. La divergence est attendue et + constitue la démonstration de la thèse du produit — ne pas la traiter + comme une erreur à réconcilier. + +allocation: + rules: + - source: pool_fees + splits: + - component: swap_fee + beneficiary: token_holder # 100% aux voters veAERO + share_source: onchain_field + field: fee_amount + - source: slipstream_unstaked + splits: + - component: swap_fee + beneficiary: lp + share_source: onchain_field + field: unstaked_fee + note: "Positions CL non-stakées : traitement distinct, ne pas confondre." + - source: external_bribes + splits: + - component: external_bribe # exclu de gross_fees par la taxonomie + beneficiary: token_holder + share_source: onchain_field + note: "Payé par des tiers. L'inclure gonflerait artificiellement Aerodrome." + + # Exemple de constante correctement encodée : bornée dans le temps, sourcée. + # Le CI échoue si source_url ou effective_from manque, ou si deux intervalles + # se chevauchent. C'est le garde-fou anti-Lido. + constants: + - name: treasury_share + value: 0.0 + effective_from: "2023-08-28T00:00:00Z" + effective_to: null + source_url: https://aerodrome.finance/docs + review_due: "2026-11-01" + +pricing: + primary: pyth_hermes + witness: chainlink + granularity: hourly_twap + fallback: pool_ratio + fallback_confidence: low + note: "Milliers de tokens sur Base. La couverture Pyth sera partielle." + +emissions: + scope: incentives_only + source: + kind: evm_logs + contract: minter + events: [Mint] + read: distributions_to_gauges + note: >- + On mesure les AERO effectivement distribués aux gauges, epoch par epoch. + PAS un taux d'inflation annualisé appliqué à la supply : le "~10.9%/an" + couramment cité est un taux d'inflation, pas un coût d'incentive, et + l'approximation ne résiste pas à un examen sérieux. + token: AERO + epoch: weekly + spread_over_epoch: true + spread_note: >- + Émissions discrètes (hebdomadaires) réparties uniformément sur 7 jours pour les + fenêtres 24h et 7j. Sans ça, le classement 24h dépendrait du jour de l'epoch. + Choix documenté et affiché sur la page méthodologie. + excluded: + - id: ve_rebase + reason: >- + Anti-dilution des lockers, pas une incentive d'acquisition de volume. + Inclus uniquement sous scope=all_inflation. + +validation: + checks: + - id: minter_conservation + kind: onchain_conservation + description: "Émissions du Minter par epoch == Σ distributions aux gauges" + tolerance_bps: 1 + note: "Doit correspondre exactement. Un écart signale un gauge manqué." + - id: voter_fee_conservation + description: "Fees distribués aux voters == Σ fees des pools" + tolerance_bps: 100 + +ui: + # Sans cette note, la ligne "lp_revenue = 0" se lit comme une donnée manquante. + annotations: + - target: lp_revenue + text: >- + Les LPs Aerodrome ne perçoivent pas de fees : ils reçoivent des émissions AERO + à la place. La valeur leur revenant est réelle mais dilutive, et apparaît en + coût d'émission plutôt qu'en revenue. + - target: net_real_revenue + text: >- + Négatif : le protocole distribue plus de valeur en émissions qu'il ne capte + en frais. C'est un résultat mesuré, pas une erreur de données. + +metadata: + chains: [base] + is_router: false + nests_within: null diff --git a/benchmarks/apps/dydx-v4.yaml b/benchmarks/apps/dydx-v4.yaml new file mode 100644 index 00000000..4a6a0e47 --- /dev/null +++ b/benchmarks/apps/dydx-v4.yaml @@ -0,0 +1,144 @@ +schema_version: 1 +slug: dydx-v4 +name: dYdX V4 +category: perps +methodology_version: 1 +status: live +verifiability: tier_2 + +# Premier protocole implémenté : REST + tout en USDC, pas d'oracle. +# Valide toute la chaîne (ledger, materializer, invariants, quarantaine) sans bruit. + +deployments: + - id: dydx-v4:dydx-chain + chain: dydx-chain + effective_from: "2023-10-26T00:00:00Z" + + finality: + mode: cosmos_finalized + lag_seconds: 10 + + source: + kind: rest + canonical: true + base_url: "https://indexer.dydx.trade/v4" + endpoints: + - /fills # source primaire + - /historicalTradingRewardAggregations/{address} + - /vault/v1/megavault/historicalPnl + pagination: by_height # curseur createdAtHeight, jamais offset temporel + note: >- + T2 : dépendance à l'indexer dYdX. Plan B : full node + indexer self-hosted + (open-source, Postgres + Redis). Objectif à 6 mois. + + witnesses: + - kind: defillama + endpoint: https://api.llama.fi/summary/fees/dydx + - kind: onchain_megavault + description: >- + Inflows USDC vers l'adresse MegaVault ≈ 50% de Σ fills.fee. + Témoin purement on-chain, indépendant de l'indexer. + +allocation: + # ATTENTION : lire on-chain via x/revshare avant de coder. + # "100% validators/stakers, 0% trésor" était faux depuis dYdX Unlimited. + rules: + - component: taker_fee + splits: + - beneficiary: lp + share_source: onchain_config + read: revshare_megavault_fraction # ~50% + param: megavault_taker_fee_share + - beneficiary: treasury + share_source: onchain_config + read: revshare_treasury_fraction # ~10% + param: treasury_taker_fee_share + - beneficiary: staker + share_source: derived # reste (~40%) + - beneficiary: third_party + component: affiliate_fee + share_source: onchain_field + field: affiliateRevShare + note: >- + Affiliés VIP jusqu'à 50% du taker fee du trader référé. + Décomposer PAR FILL, jamais en moyenne. + + - component: maker_fee + splits: + - beneficiary: lp + share_source: onchain_config + read: revshare_megavault_fraction + - beneficiary: staker + share_source: derived + + # Rebates maker (fee négatif dans fills.fee) + - component: maker_rebate + included_in_gross: false + beneficiary: lp + note: "Montant positif stocké, gross_fees = fees bruts payés avant rebate." + + # Paramètres lus quotidiennement depuis x/revshare, versionnés dans allocation_params. + # La contrainte GIST empêche deux valeurs simultanées pour le même param. + params_source: + kind: cosmos_grpc + module: x/revshare + snapshot: daily + note: "Le lire, pas le supposer. Les paramètres changent par vote de gouvernance." + + documented_split_reference: + megavault: 0.50 + treasury_subdao: 0.10 + market_mapper: 0.00 + stakers: 0.40 + source_url: https://dydx.exchange/blog/dydx-unlimited + used_for: plausibility_check_only + alert_if_deviation_gt: 0.05 + +pricing: + fees_denomination: USDC + primary: stable_passthrough + depeg_guard_bps: 50 + note: "Tout en USDC. Pas d'oracle nécessaire pour les fees." + +emissions: + scope: incentives_only + sources: + - kind: megavault_rewards + token: DYDX + note: "Rewards MegaVault en DYDX. Zéro-somme comptablement mais coût réel." + - kind: trading_rewards + token: DYDX + status: onchain_param + note: >- + Statut = paramètre de gouvernance. Le lire via x/rewards avant chaque recompute. + Ne jamais supposer actif ou inactif. + +validation: + checks: + - id: megavault_inflows + kind: onchain_conservation + description: "Inflows USDC vers MegaVault ≈ 50% de Σ fills.fee sur 24h" + tolerance_bps: 300 + note: "Meilleur test disponible : purement on-chain, indépendant de l'indexer." + - id: boundary_test + kind: unit_test + description: "3 events à t1-1ms / t1 / t1+1ms : fenêtre [t0,t1) retourne exactement 1" + severity: blocking + - id: idempotence + kind: unit_test + description: "Injecter deux fois le même event ne change aucun chiffre" + severity: blocking + - id: recompute_speed + kind: perf + description: "Recompute complet historique dYdX < 5 minutes" + - id: quarantine_trigger + kind: integration + description: "Écart artificiel de 6% injecté dans le témoin déclenche bien la quarantaine" + severity: blocking + +metadata: + chains: [dydx-chain] + is_router: false + nests_within: null + docs: https://docs.dydx.exchange + indexer: https://indexer.dydx.trade/v4 diff --git a/benchmarks/apps/gmx-v2.yaml b/benchmarks/apps/gmx-v2.yaml new file mode 100644 index 00000000..f792ef2c --- /dev/null +++ b/benchmarks/apps/gmx-v2.yaml @@ -0,0 +1,148 @@ +schema_version: 1 +slug: gmx-v2 +name: GMX V2 +category: perps +methodology_version: 1 +status: live +website: https://gmx.io +verifiability: tier_1 + +# Exemple canonique : TOUS les splits sont lus dans l'event, aucune constante. +# C'est le pattern à copier. Voir aerodrome.yaml pour le cas où c'est impossible. + +deployments: + - id: gmx-v2:arbitrum + chain: arbitrum + effective_from: "2023-08-01T00:00:00Z" + start_block: 107737756 + + finality: + mode: l1_confirmed + lag_seconds: 900 + + source: + kind: evm_logs + canonical: true + address: "0xC8ee91A54287DB53897056e12D9819156D3822Fb" # EventEmitter + # topic0 de EventLog1. Constante du contrat, pas un paramètre métier. + topic0: "0x137a44067c8961cd7e1d876f4754a5a3a75989b4552f1843fc69c3b372def160" + # topic1 = keccak256(nom). Calculé AU RUNTIME. Un hash collé en dur et mal + # recopié retourne zéro event, ce qui est indiscernable de "pas d'activité". + event_names: + - PositionFeesCollected + - SwapFeesCollected + decoder: gmx_v2_eventlog # payload = key-value typés, décodage par clé + batch_blocks: 2000 + + witnesses: + - kind: subgraph + id: gmx-v2-arbitrum + - kind: defillama + endpoint: https://api.llama.fi/summary/fees/gmx-v2 + note: "Adapter écrit par le protocole. Témoin, jamais source." + + - id: gmx-v2:avalanche + chain: avalanche + effective_from: "2023-08-01T00:00:00Z" + # Déploiement distinct, pot de fees distinct : la somme avec Arbitrum est + # correcte et ne constitue pas un double-comptage (cf. architecture §6.5). + source: + kind: evm_logs + canonical: true + address: "TODO_verifier_avant_activation" + topic0: "0x137a44067c8961cd7e1d876f4754a5a3a75989b4552f1843fc69c3b372def160" + event_names: [PositionFeesCollected, SwapFeesCollected] + decoder: gmx_v2_eventlog + status: pending_verification + +allocation: + rules: + - event: PositionFeesCollected + splits: + - field: positionFeeAmount + component: taker_fee + share_source: onchain_field # ← jamais un pourcentage + - field: borrowingFeeAmount + component: borrow_fee + share_source: onchain_field + - field: fundingFeeAmount + component: funding_payment # exclu de gross_fees par la taxonomie + share_source: onchain_field + - field: feeAmountForPool + beneficiary: lp + share_source: onchain_field + - field: feeReceiverAmount + beneficiary: token_holder # rachat GMX + share_source: onchain_field + - field: uiFeeAmount + beneficiary: third_party + component: interface_fee + share_source: onchain_field + + - event: SwapFeesCollected + splits: + - field: feeAmountForPool + component: swap_fee + beneficiary: lp + share_source: onchain_field + - field: feeReceiverAmount + component: swap_fee + beneficiary: token_holder + share_source: onchain_field + + # Contrôle intra-event. Attrape la majorité des régressions de décodage : + # si GMX ajoute un champ entre deux versions du contrat, cette assertion échoue + # avant que le chiffre ne soit publié. + assertions: + - name: position_fee_conservation + expr: "positionFeeAmount == feeAmountForPool + feeReceiverAmount + uiFeeAmount" + on_failure: quarantine + + # Le 63/27/10 documenté n'est JAMAIS appliqué au calcul. Il sert uniquement de + # test de plausibilité : un écart persistant est une information publiable. + documented_split_reference: + lp: 0.63 + token_holder: 0.27 + overhead: 0.10 + source_url: https://docs.gmx.io/docs/tokenomics + used_for: plausibility_check_only + alert_if_deviation_gt: 0.05 + +pricing: + # Collatéral mixte : conversion obligatoire. + primary: pyth_hermes + witness: chainlink + granularity: hourly_twap + chainlink_guards: + - sequencer_uptime_feed: required # Arbitrum : prix invalides si le sequencer était down + fallback: pool_ratio + fallback_confidence: low + +emissions: + scope: incentives_only + sources: + - kind: esgmx_distribution + token: GMX + accounting: at_emission # au moment de l'émission, pas du vesting + note: "Choix documenté : le coût est encouru quand le token est émis." + - kind: external_grant + token: ARB + note: >- + Programmes d'incentives Arbitrum. Les omettre flatterait net_real_revenue. + Source : distributions ARB vers les contrats GMX. + status: todo + +validation: + checks: + - id: fee_receiver_inflows + kind: onchain_conservation + description: "Inflows vers le fee receiver GMX ≈ Σ feeReceiverAmount" + tolerance_bps: 200 + - id: witness_divergence + kind: witness + quarantine_above_bps: 500 + +metadata: + chains: [arbitrum, avalanche] + is_router: false + nests_within: null diff --git a/benchmarks/apps/hyperliquid.yaml b/benchmarks/apps/hyperliquid.yaml new file mode 100644 index 00000000..f095932f --- /dev/null +++ b/benchmarks/apps/hyperliquid.yaml @@ -0,0 +1,175 @@ +schema_version: 1 +slug: hyperliquid +name: Hyperliquid +category: perps +methodology_version: 1 +status: live +verifiability: tier_2 # -> tier_1 une fois le node en place + +# Cas non-EVM. La question "plan B si l'API tombe ?" a une vraie réponse : le node. +# L'archive S3 et le node partagent le MÊME FORMAT, donc le décodeur est commun et +# bascule sans changement de code. Une panne REST coûte la fraîcheur, pas l'historique. + +deployments: + - id: hyperliquid:hypercore + chain: hyperliquid + effective_from: "2023-06-01T00:00:00Z" + + finality: + mode: hyperbft + lag_seconds: 5 + note: "Finalité quasi immédiate, mais la dépendance porte sur la source de données." + + # Sources par ordre de préférence. Le collector bascule automatiquement. + sources: + - kind: hl_node + rank: 1 + canonical: true + flags: ["--write-fills", "--batch-by-block"] + note: "Seule configuration T1. Objectif cible à 6 mois." + status: planned + + - kind: s3_archive + rank: 2 + canonical: true + buckets: + fills: "s3://hl-mainnet-node-data/node_fills_by_block" + misc: "s3://hl-mainnet-node-data/misc_events_by_block" + requester_pays: true + cost_warning: >- + ~0.8-1 GiB/jour tous coins confondus. Un backfill d'un an est une facture AWS + à trois chiffres. BUDGÉTER, et ne backfiller qu'une seule fois. + note: "Format identique à l'API. misc_events couvre transferts, staking, funding." + + - kind: rest + rank: 3 + endpoint: "https://api.hyperliquid.xyz/info" + method: POST + types: [metaAndAssetCtxs, fundingHistory, clearinghouseState, builderFills] + note: "Temps réel et métadonnées. Bornes INCLUSIVES : appeler à t1-1ms + re-filtrer." + + witnesses: + - kind: defillama + endpoint: https://api.llama.fi/summary/fees/hyperliquid + - kind: onchain_af_inflows + address: "0xfefefefefefefefefefefefefefefefefefefefe" + note: "Le meilleur témoin : purement observationnel, indépendant de l'API." + +allocation: + # ATTENTION : le brief d'origine disait "99% AF / 1% HLP". C'est incomplet ET imprécis. + # (a) L'allocation de référence est ~97%, portée vers 99% pour CERTAINES catégories + # de fees par un vote de validateurs de décembre 2025. + # (b) Le brief OMET ENTIÈREMENT les déployeurs spot et HIP-3. + rules: + - component: taker_fee + splits: + - beneficiary: burn + share_source: onchain_config + read: assistance_fund_share + note: >- + Le HYPE détenu par l'AF est formellement reconnu comme brûlé depuis un vote + pondéré par le stake (85% pour) de décembre 2025. `burn` est donc correct, + pas `token_holder`. + - beneficiary: lp + share_source: derived + target: hlp_vault + + - component: maker_fee + splits: + - beneficiary: burn + share_source: onchain_config + read: assistance_fund_share + - beneficiary: lp + share_source: derived + + # JAMBE MANQUANTE DANS LE BRIEF. Obligatoire même si elle vaut zéro. + - component: taker_fee + scope: hip3_and_spot_deployed_assets + splits: + - beneficiary: third_party + share_source: onchain_field + read: deployer_fee_share + max_observed: 0.50 + note: >- + Les déployeurs spot et HIP-3 peuvent conserver jusqu'à 50% des fees générés + par LEURS assets. Les ignorer surestime le protocole. + + # Builder fees : captés par les frontends tiers, séparés des fees protocole. + - component: interface_fee + source: builderFills + splits: + - beneficiary: third_party + is_app_layer: true + share_source: onchain_field + + documented_split_reference: + assistance_fund: 0.97 + source_url: https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees + used_for: plausibility_check_only + alert_if_deviation_gt: 0.01 + note: >- + SI L'ÉCART DÉPASSE 1%, L'ALLOCATION A CHANGÉ SANS ANNONCE. C'est exactement le + signal que le produit est censé détecter. + +pricing: + # Les fees sont en USDC : pas d'oracle nécessaire pour les fees eux-mêmes. + fees_denomination: USDC + primary: stable_passthrough + depeg_guard_bps: 50 + # En revanche les oraclePx de HL sont self-reported -> triangulation obligatoire. + oracle_triangulation: + against: pyth_hermes + assets: [BTC, ETH, SOL] + alert_if_divergence_gt: 0.01 + note: "Aucun tiers ne peut vérifier les prix de HL. Trianguler ou ne pas publier." + +emissions: + scope: incentives_only + total: 0 + review_due: "2026-11-01" + note: >- + Zéro émission d'incentive. À REVÉRIFIER TRIMESTRIELLEMENT, pas à supposer acquis. + Les unlocks mensuels aux contributeurs (~9.92M HYPE) sont de la dilution, pas un + coût d'acquisition de volume -> scope all_inflation uniquement. + +fee_taxonomy: + exclude: + - funding_payment # zéro-somme entre traders + separate_components: + - spot_fee # structure distincte, ne jamais additionner les taux + - twap_fill_fee # userTwapSliceFills : format distinct, décoder séparément + +validation: + checks: + - id: af_conservation + kind: onchain_conservation + description: "Inflows USDC de 0xfefe…fefe ≈ part AF de Σ fills.fee" + tolerance_bps: 200 + severity: blocking + note: "Test fort et purement observationnel. Le meilleur dont on dispose ici." + - id: s3_vs_rest_parity + kind: cross_source + description: "Σ fills archive S3 == Σ fills API REST sur 24h" + tolerance: exact + severity: blocking + note: >- + IDENTIQUE À L'UNITÉ PRÈS. Toute divergence signale un bug de format ou de + fenêtrage — à corriger avant toute publication. + - id: hlp_equity + kind: cross_check + description: "Équité du HLP vault (clearinghouseState) vs part résiduelle" + tolerance_bps: 500 + +ui: + annotations: + - target: token_holder_value + text: >- + L'AF reçoit de l'USDC, achète du HYPE, puis le brûle. On comptabilise la valeur + REÇUE (certaine), pas la valeur du HYPE brûlé (qui dépend du prix d'exécution). + Les deux sont exposées ; le classement utilise la première. + +metadata: + chains: [hyperliquid] + is_router: false + nests_within: null + docs: https://hyperliquid.gitbook.io/hyperliquid-docs diff --git a/benchmarks/apps/raydium.yaml b/benchmarks/apps/raydium.yaml new file mode 100644 index 00000000..1247c6bd --- /dev/null +++ b/benchmarks/apps/raydium.yaml @@ -0,0 +1,154 @@ +schema_version: 1 +slug: raydium +name: Raydium +category: dex +methodology_version: 1 +status: live +verifiability: tier_1 + +# Piège dominant : accrual vs claim. +# Les fees s'accumulent dans les vaults et sont réclamés au seuil de $10. +# Mesurer les inflows du wallet de buyback mesure des CLAIMS décalés de plusieurs jours. +# SOURCE PRIMAIRE = accrual par swap depuis AmmConfig. Les inflows = témoin cumulatif. + +deployments: + - id: raydium:solana + chain: solana + effective_from: "2021-02-01T00:00:00Z" + + finality: + mode: solana_finalized + lag_seconds: 30 + + source: + kind: solana_rpc + canonical: true + rpc: helius # Helius ou Triton, clé en env + method: getSignaturesForAddress # + getTransaction par sig + note: >- + Décoder les comptes PoolState + AmmConfig par pool. + getSignaturesForAddress sur les programmes collecteurs, puis getTransaction. + + programs: + amm_v4: + id: "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" + fee_collector: "PNLCQcVCD26aC7ZWgRyr5ptfaR7bBrWdTFgRWwu2tvF" + clmm: + id: "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK" + fee_collector: "projjosVCPQH49d5em7VYS7fJZzaqKixqKtus7yk416" + cpmm: + id: "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C" + fee_collector: "ProCXqRcXJjoUd1RNoo28bSizAA6EEqt9wURZYPDc5u" + + destinations: + ray_buyback: "DdHDoz94o2WJmD9myRobHCwtx1bESpHTd4SSPe6VEZaz" + treasury_clmm: "FundHfY8oo8J9KYGyfXFFuQCHe7Z1VBNmsj84eMcdYs4" + treasury_cpmm: "FUNDduJTA7XcckKHKfAoEnnhuSud2JUCUZv6opWEjrBU" + + witnesses: + - kind: defillama + endpoint: https://api.llama.fi/summary/fees/raydium + - kind: onchain_buyback_wallet + address: "DdHDoz94o2WJmD9myRobHCwtx1bESpHTd4SSPe6VEZaz" + note: "Témoin cumulatif des claims (décalés). Pas la source primaire." + +allocation: + # Splits lus depuis AmmConfig on-chain (trade_fee_rate, protocol_fee_rate, fund_fee_rate). + # Dénominateur 10^6. Les plateformes tierces choisissent leur cpConfigId -> lire par pool. + model: amm_config_per_pool + splits_by_pool_type: + amm_v4: + lp: 0.88 + token_holder: 0.12 # buyback RAY -> DdHDoz94 + treasury: 0.00 + source_url: https://docs.raydium.io/raydium/protocol/developers/addresses + used_for: plausibility_check_only + clmm: + lp: 0.84 + token_holder: 0.12 + treasury: 0.04 + used_for: plausibility_check_only + cpmm: + lp: 0.84 + token_holder: 0.12 + treasury: 0.04 + used_for: plausibility_check_only + + rules: + - component: swap_fee + share_source: onchain_config + read: amm_config_per_pool + note: "Lire AmmConfig par pool. Ne jamais utiliser les pourcentages ci-dessus au calcul." + splits: + - beneficiary: lp + field: lp_rate + - beneficiary: token_holder + field: protocol_fee_rate + note: >- + CORRECTION AU BRIEF : le RAY est TRANSFÉRÉ vers DdHDoz94, pas brûlé par le + mécanisme. beneficiary = token_holder, pas burn. + - beneficiary: treasury + field: fund_fee_rate + + # Creator fee CPMM : additionnel au trade fee, pas un prélèvement dessus. + - component: bonding_curve_fee + source: cpmm_creator_fee + beneficiary: creator + share_source: onchain_field + note: "L'ignorer sous-estime gross_fees." + + accrual_note: >- + COMPTABILISER À L'ACCRUAL, PAR SWAP. Pas au claim. + Les fees s'accumulent dans les vaults jusqu'au seuil de $10, puis sont transférés + par batch. La latence claim->accrual peut atteindre plusieurs jours en période calme. + Les inflows du wallet buyback servent de témoin cumulatif, pas de source primaire. + +pricing: + primary: pyth_hermes + witness: chainlink + granularity: hourly_twap + fallback: pool_ratio + fallback_confidence: low + fallback_note: >- + Memecoins Solana majoritaires en volume. pool_ratio = ratio des réserves au moment + du swap, lu on-chain. Marqué confidence:low, astérisque UI. Ne jamais inventer un prix. + token2022_note: >- + Calculer sur le montant NET entrant dans le vault (après transfer fee Token-2022), + pas sur le montant signé. + +emissions: + scope: incentives_only + sources: + - kind: farm_distributions + token: RAY + note: "Émissions des farms Raydium vers les LPs. Lire les distributions effectives." + +edge_cases: + - id: clmm_fee_growth + description: >- + CLMM : fees dans le token d'entrée seulement. Suivre fee_growth_global_0/1_x64 + séparément. Pas d'accumulation symétrique. + - id: custom_amm_config + description: "Pools tiers à config custom : lire AmmConfig par pool, obligatoirement." + - id: token2022_transfer_fee + description: "Montant net entrant dans le vault, pas montant signé." + +validation: + checks: + - id: accrual_vs_claims + kind: cross_check + description: "Accruals calculés sur 30j vs claims observés (avec décalage) < 3%" + tolerance_bps: 300 + - id: buyback_balance + kind: onchain_conservation + description: "Balance cumulative de DdHDoz94 vs Σ buybacks calculés" + tolerance_bps: 500 + - id: witness_defillama + kind: witness + quarantine_above_bps: 500 + +metadata: + chains: [solana] + is_router: false + nests_within: null + docs: https://docs.raydium.io diff --git a/benchmarks/apps/uniswap.yaml b/benchmarks/apps/uniswap.yaml new file mode 100644 index 00000000..9da2e206 --- /dev/null +++ b/benchmarks/apps/uniswap.yaml @@ -0,0 +1,201 @@ +schema_version: 1 +slug: uniswap +name: Uniswap +category: dex +methodology_version: 1 +status: live +verifiability: tier_1 + +# LE PROTOCOLE LE PLUS PIÉGEUX DU SCOPE. +# Trois axes indépendants : version (v2/v3/v4) x chaîne (~11) x date d'activation. +# Un effective_from par COMBINAISON. Appliquer un taux sur une période où il valait +# zéro est le bug Lido inversé. + +deployments: + # ------------------------------------------------------------------------- + # Ethereum mainnet v2 — adresses VÉRIFIÉES + # ------------------------------------------------------------------------- + - id: uniswap:ethereum:v2 + chain: ethereum + version: v2 + # UNIfication exécutée on-chain le 27/12/2025. AVANT : zéro protocol revenue. + effective_from: "2025-12-27T20:33:00Z" + finality: { mode: beacon_finalized, lag_seconds: 800 } + + source: + kind: evm_logs + canonical: true + # Contrat immuable où s'accumulent les fees. VÉRIFIÉ (Uniswap/protocol-fees + Etherscan). + token_jar: "0xf38521f130fcCF29dB1961597bc5d2B60F995f85" + # Releaser : brûler de l'UNI pour retirer. Destinataire 0xdead. + firepit: "0x0D5Cd355e2aBEB8fb1552F56c965B867346d6721" + read: token_jar_inflows + # On compte les INFLOWS (accrual), pas les burns Firepit (claims). Les deux + # sont décalés : le jar se vide par arbitrage quand la valeur dépasse le seuil. + + witnesses: + - kind: subgraph + id: uniswap-v2-ethereum + - kind: defillama + endpoint: https://api.llama.fi/summary/fees/uniswap + note: "Divergence attendue sur les périodes pré-fee-switch." + + # ------------------------------------------------------------------------- + # Ethereum mainnet v3 + # ------------------------------------------------------------------------- + - id: uniswap:ethereum:v3 + chain: ethereum + version: v3 + effective_from: "2025-12-27T20:33:00Z" + source: + kind: evm_logs + canonical: true + token_jar: "0xf38521f130fcCF29dB1961597bc5d2B60F995f85" + v3_fee_adapter: "0x5E74C9f42EEd283bFf3744fBD1889d398d40867d" + v3_open_fee_adapter: "0xf2371551Fe3937Db7c750f4DfABe5c2fFFdcBf5A" + read: token_jar_inflows + note: >- + SEULE UNE SÉLECTION de pools v3 a été activée (80-95% des LP fees mainnet). + Lire l'état de l'adaptateur pool par pool. Ne pas supposer une activation globale. + + # ------------------------------------------------------------------------- + # Autres chaînes et v4 — À COMPLÉTER APRÈS VÉRIFICATION + # ------------------------------------------------------------------------- + - id: uniswap:arbitrum:v3 + chain: arbitrum + version: v3 + status: pending_verification + effective_from: "TODO" + source: + kind: evm_logs + token_jar: "TODO_une_adresse_par_chaine" + note: >- + Propositions 94/95 (mars 2026) ont étendu à Arbitrum, Base, OP Mainnet, Soneium, + X Layer, Worldchain, Zora, Celo. Propositions ultérieures (96, 100...) ont ajouté + BNB Chain, Polygon et l'activation v4. + ÉNUMÉRER depuis gov.uniswap.org et les transactions d'exécution. + Récupérer chaque TokenJar depuis github.com/Uniswap/protocol-fees et le VÉRIFIER + sur un explorer avant de l'inscrire ici. + + - id: uniswap:ethereum:v4 + chain: ethereum + version: v4 + status: pending_verification + effective_from: "TODO" + note: >- + Le V4FeeAdapter était marqué work-in-progress dans le repo. Confirmer son + déploiement et sa date d'activation avant d'activer ce déploiement. + +allocation: + # ATTENTION : le protocol fee est un PRÉLÈVEMENT sur les LP fees, PAS un supplément. + # Le brief d'origine affirmait l'inverse ; le traiter comme additif surestime + # gross_fees d'environ 1/6 sur tout Uniswap. + model: carve_out_from_lp_fees + + rules: + - version: v2 + # LP fees : 0.30% -> 0.25%. 0.05% du volume va au protocole. + splits: + - beneficiary: token_holder + component: swap_fee + share_source: constant + value: 0.16667 # 0.05 / 0.30 + effective_from: "2025-12-27T20:33:00Z" + effective_to: null + source_url: https://gov.uniswap.org/t/unification-proposal/25881 + note: >- + Les fee levels v2 sont hardcodés dans les contrats et la gouvernance les + active ou désactive globalement. `constant` est ici légitime, mais borné + dans le temps et sourcé — c'est ce qui manquait au cas Lido. + - beneficiary: lp + component: swap_fee + share_source: derived # gross - protocol_fee + verify_against: token_jar_inflows + + - version: v3 + # 25% des LP fees sur les tiers 0.01% et 0.05% ; 16.7% sur 0.30% et 1%. + # Ajustable pool par pool par gouvernance -> DOIT être lu on-chain. + splits: + - beneficiary: token_holder + component: swap_fee + share_source: onchain_config + contract: v3_fee_adapter + read: protocol_fee_per_pool + fallback_reference: + tier_0_01: 0.25 + tier_0_05: 0.25 + tier_0_30: 0.167 + tier_1_00: 0.167 + used_for: plausibility_check_only + - beneficiary: lp + component: swap_fee + share_source: derived + + # Fees prélevés par des hooks v4 tiers : PAS du revenue Uniswap. + third_party_rules: + - source: v4_hooks + beneficiary: third_party + component: interface_fee + is_app_layer: true + + exclusions: + - id: treasury_burn_100m_uni + date: "2025-12-27" + reason: >- + Burn rétroactif one-off de 100M UNI depuis le trésor (~$596M), destiné à + représenter ce qui aurait été brûlé si le fee switch avait été actif depuis 2018. + CE N'EST PAS DU REVENUE. L'inclure produirait un pic absurde qui discréditerait + tout le classement. + +non_trading_revenue: + - id: unichain_sequencer + component: sequencer_revenue + beneficiary: burn + counted_in: non_trading_holder_value # HORS gross_fees + formula: "sequencer_fees - l1_data_costs - optimism_share(15%)" + rationale: >- + Entre dans le même mécanisme de burn que les protocol fees, mais ce n'est pas un + frais de trading. Additionné séparément et VISIBLEMENT à part dans l'UI. + +pricing: + primary: pyth_hermes + witness: chainlink + granularity: hourly_twap + fallback: pool_ratio + fallback_confidence: low + +emissions: + scope: incentives_only + sources: [] + total: 0 + note: "Pas de liquidity mining. emissions_cost = 0." + +interface_fees: + status: disabled + since: "2025-12-27" + note: >- + Uniswap Labs a coupé ses frontend/wallet/API fees lors d'UNIfication. Les compter + chez les concurrents fausserait la comparaison — d'où excluded_from_ranking dans + la taxonomie partagée. + +validation: + checks: + - id: zero_before_activation + kind: unit_test + description: "protocol revenue == 0 avant effective_from, PAR CHAÎNE ET PAR VERSION" + severity: blocking + - id: token_jar_conservation + kind: onchain_conservation + description: "Inflows TokenJar ≈ Σ events de collecte de protocol fee" + tolerance_bps: 200 + - id: protocol_fee_ratio_v2 + kind: plausibility + description: "protocol_fee / gross_fees ≈ 1/6 sur v2" + alert_if_deviation_gt: 0.03 + note: "Un écart signale un déploiement manqué ou un effective_from erroné." + +metadata: + chains: [ethereum] # étendre après vérification + is_router: false + nests_within: null + repo: https://github.com/Uniswap/protocol-fees diff --git a/harnesses/apps/Dockerfile b/harnesses/apps/Dockerfile new file mode 100644 index 00000000..4058acc2 --- /dev/null +++ b/harnesses/apps/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.24-alpine AS builder +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o /app/collector ./cmd/collector +RUN CGO_ENABLED=0 go build -o /app/materializer ./cmd/materializer + +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates tzdata +COPY --from=builder /app/collector /app/collector +COPY --from=builder /app/materializer /app/materializer +COPY migrations/ /app/migrations/ diff --git a/harnesses/apps/cmd/collector/main.go b/harnesses/apps/cmd/collector/main.go new file mode 100644 index 00000000..37b177f4 --- /dev/null +++ b/harnesses/apps/cmd/collector/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "time" + + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/collect/rest" + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/ledger" + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +func main() { + ctx := context.Background() + + db, err := ledger.New(ctx, mustEnv("DATABASE_URL")) + if err != nil { + log.Fatalf("collector: %v", err) + } + defer db.Close() + + go func() { + http.Handle("/metrics", promhttp.Handler()) + http.ListenAndServe(":2112", nil) + }() + + dydx := rest.NewDyDX() + + for { + if err := runDyDX(ctx, db, dydx); err != nil { + log.Printf("dydx collector error: %v", err) + } + time.Sleep(60 * time.Second) + } +} + +func runDyDX(ctx context.Context, db *ledger.DB, col *rest.DyDXCollector) error { + const deploymentID = "dydx-v4:dydx-chain" + + from, err := db.GetCursor(ctx, deploymentID) + if err != nil { + return err + } + + to := spec.Cursor{ + Height: ^uint64(0), // collect up to tip + Ts: time.Now(), + Finalized: true, + } + + out := make(chan spec.FeeEvent, 1000) + collectErr := make(chan error, 1) + + go func() { + defer close(out) + newCursor, err := col.Collect(ctx, deploymentID, from, to, out) + if err != nil { + collectErr <- err + return + } + collectErr <- db.SaveCursor(ctx, deploymentID, newCursor) + }() + + var batch []spec.FeeEvent + for e := range out { + batch = append(batch, e) + if len(batch) >= 500 { + if err := db.UpsertEvents(ctx, batch); err != nil { + return err + } + batch = batch[:0] + } + } + if len(batch) > 0 { + if err := db.UpsertEvents(ctx, batch); err != nil { + return err + } + } + + return <-collectErr +} + +func mustEnv(key string) string { + v := os.Getenv(key) + if v == "" { + log.Fatalf("missing required env: %s", key) + } + return v +} diff --git a/harnesses/apps/cmd/materializer/main.go b/harnesses/apps/cmd/materializer/main.go new file mode 100644 index 00000000..f6134538 --- /dev/null +++ b/harnesses/apps/cmd/materializer/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "time" + + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/invariant" + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/ledger" +) + +func main() { + ctx := context.Background() + + db, err := ledger.New(ctx, mustEnv("DATABASE_URL")) + if err != nil { + log.Fatalf("materializer: %v", err) + } + defer db.Close() + + checker := invariant.New(db.Pool()) + + go func() { + http.Handle("/metrics", promhttp.Handler()) + http.ListenAndServe(":2112", nil) + }() + + for { + if err := runMaterialize(ctx, db, checker); err != nil { + log.Printf("materializer error: %v", err) + } + time.Sleep(5 * time.Minute) + } +} + +func runMaterialize(ctx context.Context, db *ledger.DB, checker *invariant.Checker) error { + const mv = 1 + deployments := []string{"dydx-v4:dydx-chain"} + + for _, dep := range deployments { + if err := db.Materialize(ctx, dep, mv); err != nil { + return err + } + if err := checker.CheckAll(ctx, dep, mv); err != nil { + log.Printf("INVARIANT FAILED %s: %v — quarantining", dep, err) + if qErr := db.Quarantine(ctx, dep, err.Error()); qErr != nil { + log.Printf("quarantine write failed: %v", qErr) + } + continue + } + if err := db.RefreshMaterializedView(ctx); err != nil { + return err + } + } + return nil +} + +func mustEnv(key string) string { + v := os.Getenv(key) + if v == "" { + log.Fatalf("missing required env: %s", key) + } + return v +} diff --git a/harnesses/apps/docker-compose.apps.yml b/harnesses/apps/docker-compose.apps.yml new file mode 100644 index 00000000..dc6136f0 --- /dev/null +++ b/harnesses/apps/docker-compose.apps.yml @@ -0,0 +1,65 @@ +# Fragment docker-compose à merger dans /opt/ocb/docker-compose.yml sur le VPS. +# Données sur /data (sdb, 200GB) — isolé du disque OS. + +services: + postgres: + image: postgres:16 + container_name: ocb-postgres + restart: unless-stopped + env_file: /run/ocb/.env.apps + volumes: + - /data/postgres:/var/lib/postgresql/data + networks: [web] + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ocb -d apps"] + interval: 30s + timeout: 5s + retries: 3 + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } + + postgres-exporter: + image: prometheuscommunity/postgres-exporter:latest + container_name: ocb-postgres-exporter + restart: unless-stopped + environment: + DATA_SOURCE_NAME: "postgresql://ocb:${POSTGRES_PASSWORD}@ocb-postgres:5432/apps?sslmode=disable" + networks: [web] + expose: ["9187"] + depends_on: + postgres: + condition: service_healthy + logging: + driver: json-file + options: { max-size: "5m", max-file: "2" } + + apps-collector: + build: /opt/ocb/harnesses/apps + container_name: ocb-apps-collector + restart: unless-stopped + env_file: /run/ocb/.env.apps + command: ["/app/collector"] + networks: [web] + expose: ["2112"] + depends_on: + postgres: + condition: service_healthy + logging: + driver: json-file + options: { max-size: "20m", max-file: "5" } + + apps-materializer: + build: /opt/ocb/harnesses/apps + container_name: ocb-apps-materializer + restart: unless-stopped + env_file: /run/ocb/.env.apps + command: ["/app/materializer"] + networks: [web] + expose: ["2112"] + depends_on: + postgres: + condition: service_healthy + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } diff --git a/harnesses/apps/env.apps.example b/harnesses/apps/env.apps.example new file mode 100644 index 00000000..dc248ed6 --- /dev/null +++ b/harnesses/apps/env.apps.example @@ -0,0 +1,32 @@ +# /run/ocb/.env.apps — chiffré avec SOPS avant deploy +# Générer avec : sops encrypt --filename-override secrets/.env.apps.enc env.apps.example + +POSTGRES_USER=ocb +POSTGRES_PASSWORD=CHANGEME +POSTGRES_DB=apps +DATABASE_URL=postgresql://ocb:CHANGEME@ocb-postgres:5432/apps?sslmode=disable + +# dYdX +DYDX_INDEXER_URL=https://indexer.dydx.trade/v4 +DYDX_NODE_GRPC=https://dydx-ops-rpc.kingnodes.com:443 + +# Hyperliquid +HL_API_URL=https://api.hyperliquid.xyz/info +HL_S3_BUCKET=hl-mainnet-node-data +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION=us-east-1 + +# GMX +ARBITRUM_RPC_URL= +AVALANCHE_RPC_URL= + +# Pyth Hermes +PYTH_HERMES_URL=https://hermes.pyth.network + +# Prometheus push (si push gateway) +PROM_PUSHGATEWAY_URL=http://ocb-prom:9091 + +# Alerting +QUARANTINE_GITHUB_TOKEN= +QUARANTINE_GITHUB_REPO=ChainBench/OpenChainBench diff --git a/harnesses/apps/go.mod b/harnesses/apps/go.mod new file mode 100644 index 00000000..8344b4e0 --- /dev/null +++ b/harnesses/apps/go.mod @@ -0,0 +1,26 @@ +module github.com/ChainBench/OpenChainBench/harnesses/apps + +go 1.24.0 + +require ( + github.com/jackc/pgx/v5 v5.7.2 + github.com/prometheus/client_golang v1.20.5 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) diff --git a/harnesses/apps/go.sum b/harnesses/apps/go.sum new file mode 100644 index 00000000..9b2736a7 --- /dev/null +++ b/harnesses/apps/go.sum @@ -0,0 +1,52 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/harnesses/apps/internal/collect/rest/dydx.go b/harnesses/apps/internal/collect/rest/dydx.go new file mode 100644 index 00000000..6b6c9f32 --- /dev/null +++ b/harnesses/apps/internal/collect/rest/dydx.go @@ -0,0 +1,225 @@ +package rest + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +const dydxIndexer = "https://indexer.dydx.trade/v4" + +type DyDXCollector struct { + client *http.Client +} + +func NewDyDX() *DyDXCollector { + return &DyDXCollector{client: &http.Client{Timeout: 30 * time.Second}} +} + +func (c *DyDXCollector) Name() string { return "dydx-rest" } + +type dydxFill struct { + ID string `json:"id"` + CreatedAt string `json:"createdAt"` + CreatedAtHeight string `json:"createdAtHeight"` + Side string `json:"side"` + Liquidity string `json:"liquidity"` // TAKER | MAKER + Type string `json:"type"` + Market string `json:"market"` + Price string `json:"price"` + Size string `json:"size"` + Fee string `json:"fee"` // USDC, 6 decimals, may be negative (rebate) + SubaccountID string `json:"subaccountId"` + AffiliateRevShare string `json:"affiliateRevShare"` // USDC, optional +} + +func (c *DyDXCollector) Collect( + ctx context.Context, + deploymentID string, + from, to spec.Cursor, + out chan<- spec.FeeEvent, +) (spec.Cursor, error) { + cursor := from + pageSize := 100 + + for { + fills, nextHeight, err := c.fetchFills(ctx, cursor.Height, pageSize) + if err != nil { + return cursor, fmt.Errorf("dydx: fetch fills at height %d: %w", cursor.Height, err) + } + if len(fills) == 0 { + break + } + + for _, f := range fills { + h, _ := strconv.ParseUint(f.CreatedAtHeight, 10, 64) + // [from, to) — exclude events at or above `to` + if h >= to.Height { + return cursor, nil + } + + ts, err := time.Parse(time.RFC3339Nano, f.CreatedAt) + if err != nil { + continue + } + + // Emit taker or maker fee event + component, beneficiary := classifyDyDXFill(f) + if component == "" { + continue + } + + feeAmt := f.Fee + isRebate := len(feeAmt) > 0 && feeAmt[0] == '-' + + if isRebate { + // Maker rebate: excluded from gross_fees, beneficiary = lp + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("dydx:%s:fee", f.ID), + Ts: ts, + Height: h, + Component: "maker_rebate", + Beneficiary: "lp", + Token: "USDC", + AmountRaw: trimNegative(feeAmt), + Decimals: 6, + Market: f.Market, + Finality: spec.FinalityFinal, + Source: "dydx-indexer", + } + } else { + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("dydx:%s:fee", f.ID), + Ts: ts, + Height: h, + Component: component, + Beneficiary: beneficiary, + Token: "USDC", + AmountRaw: toMicroUSDC(feeAmt), + Decimals: 6, + Market: f.Market, + Finality: spec.FinalityFinal, + Source: "dydx-indexer", + } + } + + // Affiliate rev share (third_party leg) + if f.AffiliateRevShare != "" && f.AffiliateRevShare != "0" { + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("dydx:%s:affiliate", f.ID), + Ts: ts, + Height: h, + Component: "affiliate_fee", + Beneficiary: "third_party", + Token: "USDC", + AmountRaw: toMicroUSDC(f.AffiliateRevShare), + Decimals: 6, + Market: f.Market, + Finality: spec.FinalityFinal, + Source: "dydx-indexer", + } + } + + cursor = spec.Cursor{Height: h, Ts: ts, Finalized: true} + } + + // Advance cursor past the max height seen to avoid re-fetching the same page. + // nextHeight is the highest block in this page; next call starts at nextHeight+1. + if nextHeight == 0 || nextHeight+1 >= to.Height { + break + } + cursor.Height = nextHeight + 1 + } + + return cursor, nil +} + +func (c *DyDXCollector) fetchFills(ctx context.Context, fromHeight uint64, limit int) ([]dydxFill, uint64, error) { + url := fmt.Sprintf("%s/fills?limit=%d&createdAtHeight=%d", dydxIndexer, limit, fromHeight) + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + resp, err := c.client.Do(req) + if err != nil { + return nil, 0, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, 0, fmt.Errorf("HTTP %d", resp.StatusCode) + } + + var result struct { + Fills []dydxFill `json:"fills"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, 0, err + } + + var maxHeight uint64 + for _, f := range result.Fills { + h, _ := strconv.ParseUint(f.CreatedAtHeight, 10, 64) + if h > maxHeight { + maxHeight = h + } + } + + return result.Fills, maxHeight, nil +} + +func classifyDyDXFill(f dydxFill) (component, beneficiary string) { + switch f.Liquidity { + case "TAKER": + return "taker_fee", "staker" // split handled by materializer via allocation_params + case "MAKER": + return "maker_fee", "lp" + } + return "", "" +} + +// toMicroUSDC converts a decimal USDC string (e.g. "1.234567") to integer micro-USDC +// without using float64 to avoid rounding errors in accounting code. +// dYdX indexer always returns exactly 6 decimal places. +func toMicroUSDC(s string) string { + if s == "" { + return "0" + } + dot := -1 + for i, c := range s { + if c == '.' { + dot = i + break + } + } + if dot == -1 { + // No decimal point — multiply by 1e6 + return s + "000000" + } + intPart := s[:dot] + fracPart := s[dot+1:] + // Pad or truncate to exactly 6 decimal places + for len(fracPart) < 6 { + fracPart += "0" + } + fracPart = fracPart[:6] + // Remove leading zeros from intPart to avoid octal interpretation, then combine + result := intPart + fracPart + // Strip leading zeros (but keep at least one digit) + for len(result) > 1 && result[0] == '0' { + result = result[1:] + } + return result +} + +func trimNegative(s string) string { + if len(s) > 0 && s[0] == '-' { + return s[1:] + } + return s +} diff --git a/harnesses/apps/internal/invariant/checks.go b/harnesses/apps/internal/invariant/checks.go new file mode 100644 index 00000000..732fdab4 --- /dev/null +++ b/harnesses/apps/internal/invariant/checks.go @@ -0,0 +1,130 @@ +package invariant + +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var failures = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "ocb_app_invariant_failures_total", + Help: "Number of accounting invariant failures by deployment and invariant name.", +}, []string{"deployment", "invariant"}) + +type Checker struct { + pool *pgxpool.Pool +} + +func New(pool *pgxpool.Pool) *Checker { + return &Checker{pool: pool} +} + +// CheckAll runs all invariants for a deployment after a materializer run. +// Returns the first error found, or nil if all pass. +func (c *Checker) CheckAll(ctx context.Context, deploymentID string, methodologyVersion int) error { + if err := c.checkI1GrossSum(ctx, deploymentID, methodologyVersion); err != nil { + failures.WithLabelValues(deploymentID, "I1").Inc() + return fmt.Errorf("I1 (gross sum): %w", err) + } + if err := c.checkI4Additivity(ctx, deploymentID, methodologyVersion); err != nil { + failures.WithLabelValues(deploymentID, "I4").Inc() + return fmt.Errorf("I4 (additivity): %w", err) + } + if err := c.checkI3NoNegatives(ctx, deploymentID); err != nil { + failures.WithLabelValues(deploymentID, "I3").Inc() + return fmt.Errorf("I3 (no negatives): %w", err) + } + return nil +} + +// I1: Σ(beneficiary amounts) == gross_fees — exact, not approximate. +// Any discrepancy reveals a decoder bug we want to see. +func (c *Checker) checkI1GrossSum(ctx context.Context, deploymentID string, mv int) error { + var unallocatedBPS float64 + err := c.pool.QueryRow(ctx, ` + WITH gross AS ( + SELECT SUM(amount_usd) AS total + FROM fee_facts + WHERE deployment_id = $1 + AND methodology_version = $2 + AND component NOT IN ('funding_payment','external_bribe','sequencer_revenue','maker_rebate') + ), + allocated AS ( + SELECT SUM(amount_usd) AS total + FROM fee_facts + WHERE deployment_id = $1 + AND methodology_version = $2 + ) + SELECT CASE WHEN gross.total > 0 + THEN ABS(gross.total - allocated.total) / gross.total * 10000 + ELSE 0 END + FROM gross, allocated`, + deploymentID, mv, + ).Scan(&unallocatedBPS) + if err != nil { + return err + } + if unallocatedBPS > 50 { + return fmt.Errorf("unallocated = %.1f bps (threshold 50 bps)", unallocatedBPS) + } + return nil +} + +// I4: sum of hourly buckets == sum of the full window — exact, not epsilon. +// Tests that [t0,t1) windows compose correctly. +func (c *Checker) checkI4Additivity(ctx context.Context, deploymentID string, mv int) error { + var diff float64 + err := c.pool.QueryRow(ctx, ` + WITH full_window AS ( + SELECT SUM(amount_usd) AS total + FROM fee_facts + WHERE deployment_id = $1 + AND methodology_version = $2 + AND bucket_start >= now() - INTERVAL '24 hours' + AND bucket_start < now() + ), + sum_of_halves AS ( + SELECT + SUM(CASE WHEN bucket_start < now() - INTERVAL '12 hours' THEN amount_usd ELSE 0 END) + + SUM(CASE WHEN bucket_start >= now() - INTERVAL '12 hours' THEN amount_usd ELSE 0 END) + AS total + FROM fee_facts + WHERE deployment_id = $1 + AND methodology_version = $2 + AND bucket_start >= now() - INTERVAL '24 hours' + AND bucket_start < now() + ) + SELECT ABS(COALESCE(full_window.total,0) - COALESCE(sum_of_halves.total,0)) + FROM full_window, sum_of_halves`, + deploymentID, mv, + ).Scan(&diff) + if err != nil { + return err + } + if diff != 0 { + return fmt.Errorf("additivity broken: diff = %f (must be exactly 0)", diff) + } + return nil +} + +// I3: no negative amounts except maker_rebate. +func (c *Checker) checkI3NoNegatives(ctx context.Context, deploymentID string) error { + var count int + err := c.pool.QueryRow(ctx, ` + SELECT COUNT(*) FROM fee_events + WHERE deployment_id = $1 + AND amount_raw < 0 + AND component != 'maker_rebate'`, + deploymentID, + ).Scan(&count) + if err != nil { + return err + } + if count > 0 { + return fmt.Errorf("%d events with negative amount_raw (non-rebate)", count) + } + return nil +} diff --git a/harnesses/apps/internal/ledger/ledger.go b/harnesses/apps/internal/ledger/ledger.go new file mode 100644 index 00000000..b8c3c825 --- /dev/null +++ b/harnesses/apps/internal/ledger/ledger.go @@ -0,0 +1,148 @@ +package ledger + +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +type DB struct { + pool *pgxpool.Pool +} + +func New(ctx context.Context, connStr string) (*DB, error) { + pool, err := pgxpool.New(ctx, connStr) + if err != nil { + return nil, fmt.Errorf("ledger: connect: %w", err) + } + return &DB{pool: pool}, nil +} + +func (db *DB) Close() { db.pool.Close() } + +// UpsertEvents inserts fee events idempotently. +// A duplicate (deployment_id, event_key, beneficiary) is silently ignored. +func (db *DB) UpsertEvents(ctx context.Context, events []spec.FeeEvent) error { + if len(events) == 0 { + return nil + } + + tx, err := db.pool.Begin(ctx) + if err != nil { + return fmt.Errorf("ledger: begin: %w", err) + } + defer tx.Rollback(ctx) + + for _, e := range events { + _, err := tx.Exec(ctx, ` + INSERT INTO fee_events + (deployment_id, event_key, beneficiary, ts, height, + component, token, amount_raw, decimals, market, + finality, source, meta) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8::numeric,$9,$10,$11,$12,$13) + ON CONFLICT (deployment_id, event_key, beneficiary) DO NOTHING`, + e.DeploymentID, e.EventKey, e.Beneficiary, e.Ts, e.Height, + e.Component, e.Token, e.AmountRaw, e.Decimals, e.Market, + string(e.Finality), e.Source, metaToJSON(e.Meta), + ) + if err != nil { + return fmt.Errorf("ledger: upsert event %s: %w", e.EventKey, err) + } + } + + return tx.Commit(ctx) +} + +// GetCursor returns the last known cursor for a deployment. +func (db *DB) GetCursor(ctx context.Context, deploymentID string) (spec.Cursor, error) { + var c spec.Cursor + err := db.pool.QueryRow(ctx, ` + SELECT height, COALESCE(block_hash,''), ts, finalized + FROM collector_cursors WHERE deployment_id = $1`, deploymentID, + ).Scan(&c.Height, &c.BlockHash, &c.Ts, &c.Finalized) + if err != nil { + return spec.Cursor{}, nil // no cursor yet = start from genesis + } + return c, nil +} + +// SaveCursor upserts the cursor after a successful batch. +func (db *DB) SaveCursor(ctx context.Context, deploymentID string, c spec.Cursor) error { + _, err := db.pool.Exec(ctx, ` + INSERT INTO collector_cursors (deployment_id, height, block_hash, ts, finalized, updated_at) + VALUES ($1,$2,$3,$4,$5,now()) + ON CONFLICT (deployment_id) DO UPDATE SET + height=EXCLUDED.height, block_hash=EXCLUDED.block_hash, + ts=EXCLUDED.ts, finalized=EXCLUDED.finalized, updated_at=now()`, + deploymentID, c.Height, c.BlockHash, c.Ts, c.Finalized, + ) + return err +} + +// Pool exposes the underlying pool for the invariant checker. +func (db *DB) Pool() *pgxpool.Pool { return db.pool } + +// Materialize computes fee_facts from fee_events for a deployment. +// Prices are stored as 1.0 for USDC (stable_passthrough); extend for non-stable tokens. +func (db *DB) Materialize(ctx context.Context, deploymentID string, mv int) error { + _, err := db.pool.Exec(ctx, ` + INSERT INTO fee_facts + (deployment_id, bucket_start, methodology_version, + component, beneficiary, token, + amount_native, price_usd, price_source, price_ts, price_confidence, + amount_usd, event_count, finality) + SELECT + deployment_id, + date_trunc('hour', ts) AS bucket_start, + $2 AS methodology_version, + component, + beneficiary, + token, + SUM(amount_raw::numeric / POW(10, decimals)) AS amount_native, + 1.0 AS price_usd, + 'stable_passthrough' AS price_source, + date_trunc('hour', ts) AS price_ts, + 'high' AS price_confidence, + SUM(amount_raw::numeric / POW(10, decimals)) AS amount_usd, + COUNT(*) AS event_count, + MIN(finality) AS finality + FROM fee_events + WHERE deployment_id = $1 + GROUP BY deployment_id, date_trunc('hour', ts), component, beneficiary, token + ON CONFLICT (deployment_id, bucket_start, methodology_version, component, beneficiary, token) + DO UPDATE SET + amount_native = EXCLUDED.amount_native, + amount_usd = EXCLUDED.amount_usd, + event_count = EXCLUDED.event_count, + finality = EXCLUDED.finality`, + deploymentID, mv, + ) + return err +} + +// Quarantine inserts a quarantine record for a deployment bucket. +func (db *DB) Quarantine(ctx context.Context, deploymentID, reason string) error { + _, err := db.pool.Exec(ctx, ` + INSERT INTO quarantine (deployment_id, bucket_start, reason, opened_at) + VALUES ($1, date_trunc('hour', now()), $2, now()) + ON CONFLICT (deployment_id, bucket_start, reason) DO NOTHING`, + deploymentID, reason, + ) + return err +} + +// RefreshMaterializedView refreshes fee_facts_windowed. +func (db *DB) RefreshMaterializedView(ctx context.Context) error { + _, err := db.pool.Exec(ctx, `REFRESH MATERIALIZED VIEW CONCURRENTLY fee_facts_windowed`) + return err +} + +func metaToJSON(m map[string]string) interface{} { + if len(m) == 0 { + return nil + } + return m +} diff --git a/harnesses/apps/internal/spec/types.go b/harnesses/apps/internal/spec/types.go new file mode 100644 index 00000000..9177ee7d --- /dev/null +++ b/harnesses/apps/internal/spec/types.go @@ -0,0 +1,55 @@ +package spec + +import ( + "context" + "time" +) + +// HalfOpenRange enforces [From, To) semantics everywhere. +// No function in this codebase accepts two bare time.Time values for a window. +type HalfOpenRange struct { + From time.Time // inclusive + To time.Time // exclusive +} + +func (r HalfOpenRange) Contains(t time.Time) bool { + return !t.Before(r.From) && t.Before(r.To) +} + +type Finality string + +const ( + FinalityProvisional Finality = "provisional" + FinalityFinal Finality = "final" +) + +// FeeEvent is a raw fact in native units. No USD, no float. +type FeeEvent struct { + DeploymentID string + EventKey string // chain:tx_hash:log_index:sub — idempotence key + Ts time.Time + Height uint64 + Component string + Beneficiary string + Token string + AmountRaw string // NUMERIC(78,0) as string — never float64 + Decimals uint8 + Market string + Finality Finality + Source string + Meta map[string]string +} + +type Cursor struct { + Height uint64 + BlockHash string + Ts time.Time + Finalized bool +} + +// Collector is the only interface collectors implement. +// `to` is EXCLUDED — all windows are [from, to). +type Collector interface { + Name() string + Collect(ctx context.Context, deploymentID string, from, to Cursor, out chan<- FeeEvent) (Cursor, error) +} diff --git a/harnesses/apps/migrations/001_init.sql b/harnesses/apps/migrations/001_init.sql new file mode 100644 index 00000000..fbea9b6a --- /dev/null +++ b/harnesses/apps/migrations/001_init.sql @@ -0,0 +1,181 @@ +-- OpenChainBench /apps — ledger initial +-- Principe : fee_events est immuable et fait autorité. Tout le reste est dérivé +-- et reconstructible. Aucun UPDATE sur fee_events, uniquement des upserts idempotents. + +BEGIN; + +-- --------------------------------------------------------------------------- +-- Ledger brut. Unités natives, aucune conversion, aucun float. +-- --------------------------------------------------------------------------- +CREATE TABLE fee_events ( + deployment_id TEXT NOT NULL, + event_key TEXT NOT NULL, -- chain:tx_hash:log_index:sub + beneficiary TEXT NOT NULL, + ts TIMESTAMPTZ NOT NULL, -- timestamp du bloc, UTC + height BIGINT NOT NULL, + component TEXT NOT NULL, + token TEXT NOT NULL, + amount_raw NUMERIC(78,0) NOT NULL, -- entier natif. JAMAIS de float ici. + decimals SMALLINT NOT NULL, + market TEXT, + finality TEXT NOT NULL DEFAULT 'provisional', + source TEXT NOT NULL, + ingested_at TIMESTAMPTZ NOT NULL DEFAULT now(), + meta JSONB, + -- beneficiary fait partie de la clé : un même log émet souvent plusieurs + -- allocations (feeAmountForPool + feeReceiverAmount dans un PositionFeesCollected). + PRIMARY KEY (deployment_id, event_key, beneficiary), + CONSTRAINT finality_valid CHECK (finality IN ('provisional','final')) +); + +CREATE INDEX fee_events_ts ON fee_events (deployment_id, ts); +CREATE INDEX fee_events_height ON fee_events (deployment_id, height); +CREATE INDEX fee_events_provisional ON fee_events (deployment_id, height) + WHERE finality = 'provisional'; -- pour le rembobinage après reorg + +-- --------------------------------------------------------------------------- +-- Faits dérivés. Jetables : DELETE + recompute est une opération normale. +-- --------------------------------------------------------------------------- +CREATE TABLE fee_facts ( + deployment_id TEXT NOT NULL, + bucket_start TIMESTAMPTZ NOT NULL, -- bucket horaire, semi-ouvert [start, start+1h) + methodology_version INT NOT NULL, + component TEXT NOT NULL, + beneficiary TEXT NOT NULL, + token TEXT NOT NULL, + amount_native NUMERIC NOT NULL, + -- Le prix utilisé est stocké. Sans lui, un chiffre USD n'est pas reproductible + -- et donc pas auditable — c'est tout l'argument du produit. + price_usd NUMERIC NOT NULL, + price_source TEXT NOT NULL, + price_ts TIMESTAMPTZ NOT NULL, + price_confidence TEXT NOT NULL DEFAULT 'high', + amount_usd NUMERIC NOT NULL, + event_count INT NOT NULL, + finality TEXT NOT NULL, + PRIMARY KEY (deployment_id, bucket_start, methodology_version, + component, beneficiary, token), + CONSTRAINT price_confidence_valid CHECK (price_confidence IN ('high','medium','low')) +); + +CREATE INDEX fee_facts_bucket ON fee_facts (bucket_start, methodology_version); + +-- Témoins. Table séparée : jamais jointe aux faits canoniques, jamais sommée avec eux. +CREATE TABLE fee_facts_witness ( + deployment_id TEXT NOT NULL, + bucket_start TIMESTAMPTZ NOT NULL, + witness_source TEXT NOT NULL, + metric TEXT NOT NULL, + amount_usd NUMERIC NOT NULL, + fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (deployment_id, bucket_start, witness_source, metric) +); + +-- --------------------------------------------------------------------------- +-- Émissions. tokens_emitted est brut et séparé du prix : n'importe qui doit +-- pouvoir re-pricer avec sa propre source. +-- --------------------------------------------------------------------------- +CREATE TABLE emissions_daily ( + deployment_id TEXT NOT NULL, + day DATE NOT NULL, + token TEXT NOT NULL, + scope TEXT NOT NULL, -- incentives_only | all_inflation + tokens_emitted NUMERIC NOT NULL, + price_usd NUMERIC NOT NULL, + price_min_usd NUMERIC, -- bande d'incertitude si volatilité > 15% + price_max_usd NUMERIC, + price_method TEXT NOT NULL DEFAULT 'daily_twap', + cost_usd NUMERIC NOT NULL, + PRIMARY KEY (deployment_id, day, token, scope) +); + +-- --------------------------------------------------------------------------- +-- Paramètres d'allocation lus on-chain et versionnés dans le temps. +-- C'est la table qui empêche le bug Lido : aucun taux n'est appliqué en dehors +-- de son intervalle de validité observé. +-- --------------------------------------------------------------------------- +CREATE TABLE allocation_params ( + deployment_id TEXT NOT NULL, + param TEXT NOT NULL, + value NUMERIC NOT NULL, + valid_from TIMESTAMPTZ NOT NULL, + valid_to TIMESTAMPTZ, + observed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + source TEXT NOT NULL, -- contrat/méthode lue, ou URL de gouvernance + PRIMARY KEY (deployment_id, param, valid_from) +); + +-- Interdit deux valeurs simultanées pour un même paramètre. +CREATE EXTENSION IF NOT EXISTS btree_gist; +ALTER TABLE allocation_params + ADD CONSTRAINT allocation_params_no_overlap + EXCLUDE USING gist ( + deployment_id WITH =, + param WITH =, + tstzrange(valid_from, COALESCE(valid_to, 'infinity'::timestamptz)) WITH && + ); + +-- --------------------------------------------------------------------------- +-- Curseurs et audit +-- --------------------------------------------------------------------------- +CREATE TABLE collector_cursors ( + deployment_id TEXT PRIMARY KEY, + height BIGINT NOT NULL, + block_hash TEXT, -- détection de reorg + ts TIMESTAMPTZ NOT NULL, + finalized BOOLEAN NOT NULL DEFAULT false, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE methodology_versions ( + slug TEXT NOT NULL, + version INT NOT NULL, + applied_at TIMESTAMPTZ NOT NULL DEFAULT now(), + summary TEXT NOT NULL, + diff_json JSONB NOT NULL, -- alimente /apps/changelog + PRIMARY KEY (slug, version) +); + +CREATE TABLE quarantine ( + deployment_id TEXT NOT NULL, + bucket_start TIMESTAMPTZ NOT NULL, + reason TEXT NOT NULL, + detail JSONB, + issue_url TEXT, + opened_at TIMESTAMPTZ NOT NULL DEFAULT now(), + resolved_at TIMESTAMPTZ, + PRIMARY KEY (deployment_id, bucket_start, reason) +); + +-- --------------------------------------------------------------------------- +-- Vue de service du leaderboard. ~40 lignes pour le MVP. +-- Rafraîchie à chaque run du materializer ; la page ne calcule rien. +-- --------------------------------------------------------------------------- +CREATE MATERIALIZED VIEW fee_facts_windowed AS +SELECT + f.deployment_id, + w.window_label, + f.methodology_version, + SUM(f.amount_usd) FILTER (WHERE f.component NOT IN + ('funding_payment','external_bribe','sequencer_revenue','maker_rebate')) AS gross_fees_usd, + SUM(f.amount_usd) FILTER (WHERE f.beneficiary = 'lp') AS lp_revenue_usd, + SUM(f.amount_usd) FILTER (WHERE f.beneficiary IN ('treasury','insurance_fund')) AS protocol_cut_usd, + SUM(f.amount_usd) FILTER (WHERE f.beneficiary IN ('token_holder','staker','burn')) AS token_holder_value_usd, + MIN(f.finality) AS finality, + SUM(f.event_count) AS event_count +FROM fee_facts f +CROSS JOIN LATERAL ( + VALUES ('24h', INTERVAL '24 hours'), + ('7d', INTERVAL '7 days'), + ('30d', INTERVAL '30 days') +) AS w(window_label, span) +-- Fenêtre SEMI-OUVERTE. Le `<` de droite est le correctif du bug de +-- double-comptage à la seconde frontière (DefiLlama issue #8656). +WHERE f.bucket_start >= date_trunc('hour', now()) - w.span + AND f.bucket_start < date_trunc('hour', now()) +GROUP BY f.deployment_id, w.window_label, f.methodology_version; + +CREATE UNIQUE INDEX fee_facts_windowed_pk + ON fee_facts_windowed (deployment_id, window_label, methodology_version); + +COMMIT; diff --git a/harnesses/apps/prom-rules/alert_rules_apps.yml b/harnesses/apps/prom-rules/alert_rules_apps.yml new file mode 100644 index 00000000..34e0b4f0 --- /dev/null +++ b/harnesses/apps/prom-rules/alert_rules_apps.yml @@ -0,0 +1,151 @@ +groups: + - name: apps_infrastructure + rules: + - alert: AppsPostgresDown + expr: pg_up{job="postgres-exporter"} == 0 + for: 2m + labels: + severity: critical + team: ocb + annotations: + summary: "Postgres OCB-Apps est down" + description: "pg_up == 0 depuis 2 minutes. Collectors et materializer sont bloqués." + + - alert: AppsDataDiskWarning + expr: (node_filesystem_avail_bytes{mountpoint="/data"} / node_filesystem_size_bytes{mountpoint="/data"}) < 0.20 + for: 5m + labels: + severity: warning + team: ocb + annotations: + summary: "/data à plus de 80% de capacité" + description: "Disponible : {{ $value | humanize }}B. Prévoir extension ou nettoyage." + + - alert: AppsDataDiskCritical + expr: (node_filesystem_avail_bytes{mountpoint="/data"} / node_filesystem_size_bytes{mountpoint="/data"}) < 0.10 + for: 2m + labels: + severity: critical + team: ocb + annotations: + summary: "/data à plus de 90% — Postgres risque de crasher" + description: "Action immédiate requise." + + - alert: AppsPostgresConnSaturation + expr: pg_stat_database_numbackends{datname="apps"} / pg_settings_max_connections > 0.80 + for: 5m + labels: + severity: warning + team: ocb + annotations: + summary: "Postgres : connexions > 80% de max_connections" + + - alert: AppsDBSizeGrowthAnomaly + expr: deriv(pg_database_size_bytes{datname="apps"}[1h]) > 524288000 + for: 10m + labels: + severity: warning + team: ocb + annotations: + summary: "Postgres apps croît de >500MB/h" + description: "Probable bug de replay. Vérifier l'idempotence du collector." + + - name: apps_collectors + rules: + - alert: AppsCollectorLagWarning + expr: time() - ocb_app_data_freshness_seconds > 600 + for: 5m + labels: + severity: warning + team: ocb + annotations: + summary: "Collector /apps en retard > 10min" + description: "Deployment {{ $labels.deployment }} — dernier event il y a {{ $value | humanizeDuration }}." + + - alert: AppsCollectorDead + expr: time() - ocb_app_data_freshness_seconds > 3600 + for: 2m + labels: + severity: critical + team: ocb + annotations: + summary: "Collector /apps mort (> 1h sans données)" + description: "Deployment {{ $labels.deployment }} silencieux depuis {{ $value | humanizeDuration }}." + + - alert: AppsCollectorErrors + expr: rate(ocb_app_collector_errors_total[5m]) > 0.1 + for: 10m + labels: + severity: warning + team: ocb + annotations: + summary: "Erreurs collector /apps" + description: "{{ $labels.deployment }} / {{ $labels.kind }} : {{ $value | humanize }} err/s" + + - alert: AppsMaterializerStale + expr: time() - ocb_app_materializer_last_run_seconds > 1800 + for: 2m + labels: + severity: warning + team: ocb + annotations: + summary: "Materializer /apps n'a pas tourné depuis > 30min" + + - name: apps_data_quality + rules: + - alert: AppsInvariantFailed + expr: ocb_app_invariant_failures_total > 0 + for: 0m + labels: + severity: critical + team: ocb + annotations: + summary: "Invariant comptable cassé sur /apps" + description: >- + {{ $labels.deployment }} — invariant {{ $labels.invariant }}. + I1 (Σ bénéficiaires ≠ gross) ou I4 (additivité sous-fenêtres cassée). + Le chiffre NE DOIT PAS être publié. Quarantaine automatique en cours. + + - alert: AppsQuarantineOpen + expr: ocb_app_quarantined > 0 + for: 24h + labels: + severity: warning + team: ocb + annotations: + summary: "Ligne /apps en quarantaine non résolue > 24h" + description: "Deployment {{ $labels.deployment }} — voir issue GitHub liée." + + - alert: AppsWitnessDivergenceHigh + expr: ocb_app_witness_divergence_ratio > 0.05 + for: 30m + labels: + severity: warning + team: ocb + annotations: + summary: "Divergence témoin > 5% sur /apps" + description: >- + {{ $labels.deployment }} vs {{ $labels.witness }} : {{ $value | humanizePercentage }}. + Déclenchement automatique de la quarantaine si persistant. + + - alert: AppsUnallocatedFeeHigh + expr: ocb_app_unallocated_bps > 50 + for: 10m + labels: + severity: warning + team: ocb + annotations: + summary: "/apps : fees non alloués > 50 bps" + description: "{{ $labels.deployment }} — {{ $value }} bps non alloués. Bug de décodage probable." + + - alert: AppsZeroEventsOnActiveDeployment + expr: rate(ocb_app_collector_events_total[1h]) == 0 + for: 2h + labels: + severity: warning + team: ocb + annotations: + summary: "Zéro event collecté sur un déploiement actif" + description: >- + {{ $labels.deployment }} : aucun event depuis 2h. + Vérifier que les topic0 / hashes sont calculés au runtime et non zéro. From adb7335906f1b84457cc044b8fa35fec81ddae14 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:27:08 +0200 Subject: [PATCH 13/39] fix: dydx collector json key megavaultPnl, cumulative-to-delta PnL, Decimals=0 --- harnesses/apps/internal/collect/rest/dydx.go | 238 +++++++------------ 1 file changed, 86 insertions(+), 152 deletions(-) diff --git a/harnesses/apps/internal/collect/rest/dydx.go b/harnesses/apps/internal/collect/rest/dydx.go index 6b6c9f32..07e660c0 100644 --- a/harnesses/apps/internal/collect/rest/dydx.go +++ b/harnesses/apps/internal/collect/rest/dydx.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "time" "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" @@ -23,19 +24,16 @@ func NewDyDX() *DyDXCollector { func (c *DyDXCollector) Name() string { return "dydx-rest" } -type dydxFill struct { - ID string `json:"id"` - CreatedAt string `json:"createdAt"` - CreatedAtHeight string `json:"createdAtHeight"` - Side string `json:"side"` - Liquidity string `json:"liquidity"` // TAKER | MAKER - Type string `json:"type"` - Market string `json:"market"` - Price string `json:"price"` - Size string `json:"size"` - Fee string `json:"fee"` // USDC, 6 decimals, may be negative (rebate) - SubaccountID string `json:"subaccountId"` - AffiliateRevShare string `json:"affiliateRevShare"` // USDC, optional +// megavaultPnlEntry is one day of MegaVault historical PnL. +// MegaVault receives ~50% of all taker fees — tracking its PnL gives us lp_revenue directly. +// Source: GET /v4/vault/v1/megavault/historicalPnl +type megavaultPnlEntry struct { + Equity string `json:"equity"` + TotalPnl string `json:"totalPnl"` + NetTransfers string `json:"netTransfers"` + CreatedAt string `json:"createdAt"` // "2026-01-15T00:00:00.000Z" + BlockHeight string `json:"blockHeight"` + BlockTime string `json:"blockTime"` } func (c *DyDXCollector) Collect( @@ -45,181 +43,117 @@ func (c *DyDXCollector) Collect( out chan<- spec.FeeEvent, ) (spec.Cursor, error) { cursor := from - pageSize := 100 - for { - fills, nextHeight, err := c.fetchFills(ctx, cursor.Height, pageSize) - if err != nil { - return cursor, fmt.Errorf("dydx: fetch fills at height %d: %w", cursor.Height, err) + entries, err := c.fetchMegavaultPnl(ctx) + if err != nil { + return cursor, fmt.Errorf("dydx: megavault pnl: %w", err) + } + + // totalPnl is cumulative; emit daily deltas only. + // entries arrive oldest-first from the indexer. + for i, e := range entries { + h, _ := strconv.ParseUint(e.BlockHeight, 10, 64) + if h < from.Height { + continue } - if len(fills) == 0 { + if h >= to.Height { break } - for _, f := range fills { - h, _ := strconv.ParseUint(f.CreatedAtHeight, 10, 64) - // [from, to) — exclude events at or above `to` - if h >= to.Height { - return cursor, nil - } - - ts, err := time.Parse(time.RFC3339Nano, f.CreatedAt) + ts, err := parseTime(e.BlockTime) + if err != nil { + ts, err = parseTime(e.CreatedAt) if err != nil { continue } + } - // Emit taker or maker fee event - component, beneficiary := classifyDyDXFill(f) - if component == "" { - continue - } - - feeAmt := f.Fee - isRebate := len(feeAmt) > 0 && feeAmt[0] == '-' - - if isRebate { - // Maker rebate: excluded from gross_fees, beneficiary = lp - out <- spec.FeeEvent{ - DeploymentID: deploymentID, - EventKey: fmt.Sprintf("dydx:%s:fee", f.ID), - Ts: ts, - Height: h, - Component: "maker_rebate", - Beneficiary: "lp", - Token: "USDC", - AmountRaw: trimNegative(feeAmt), - Decimals: 6, - Market: f.Market, - Finality: spec.FinalityFinal, - Source: "dydx-indexer", - } - } else { - out <- spec.FeeEvent{ - DeploymentID: deploymentID, - EventKey: fmt.Sprintf("dydx:%s:fee", f.ID), - Ts: ts, - Height: h, - Component: component, - Beneficiary: beneficiary, - Token: "USDC", - AmountRaw: toMicroUSDC(feeAmt), - Decimals: 6, - Market: f.Market, - Finality: spec.FinalityFinal, - Source: "dydx-indexer", - } - } - - // Affiliate rev share (third_party leg) - if f.AffiliateRevShare != "" && f.AffiliateRevShare != "0" { - out <- spec.FeeEvent{ - DeploymentID: deploymentID, - EventKey: fmt.Sprintf("dydx:%s:affiliate", f.ID), - Ts: ts, - Height: h, - Component: "affiliate_fee", - Beneficiary: "third_party", - Token: "USDC", - AmountRaw: toMicroUSDC(f.AffiliateRevShare), - Decimals: 6, - Market: f.Market, - Finality: spec.FinalityFinal, - Source: "dydx-indexer", - } - } - - cursor = spec.Cursor{Height: h, Ts: ts, Finalized: true} + // Daily delta = this snapshot's cumulative PnL minus previous snapshot's. + // Skip first entry (no previous to diff against) or loss days. + if i == 0 { + continue + } + delta := pnlDelta(entries[i-1].TotalPnl, e.TotalPnl) + if delta == "" || strings.HasPrefix(delta, "-") || delta == "0" { + continue } - // Advance cursor past the max height seen to avoid re-fetching the same page. - // nextHeight is the highest block in this page; next call starts at nextHeight+1. - if nextHeight == 0 || nextHeight+1 >= to.Height { - break + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("dydx:megavault:%s", e.BlockHeight), + Ts: ts, + Height: h, + Component: "taker_fee", + Beneficiary: "lp", + Token: "USDC", + AmountRaw: delta, + Decimals: 0, + Market: "all", + Finality: spec.FinalityFinal, + Source: "dydx-indexer-megavault-pnl", + Meta: map[string]string{ + "note": "MegaVault daily PnL delta ~ 50% of taker fees. Full split applied by materializer.", + "equity": e.Equity, + "net_transfers": e.NetTransfers, + "total_pnl": e.TotalPnl, + }, } - cursor.Height = nextHeight + 1 + + cursor = spec.Cursor{Height: h + 1, Ts: ts, Finalized: true} } return cursor, nil } -func (c *DyDXCollector) fetchFills(ctx context.Context, fromHeight uint64, limit int) ([]dydxFill, uint64, error) { - url := fmt.Sprintf("%s/fills?limit=%d&createdAtHeight=%d", dydxIndexer, limit, fromHeight) - req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) +func (c *DyDXCollector) fetchMegavaultPnl(ctx context.Context) ([]megavaultPnlEntry, error) { + url := fmt.Sprintf("%s/vault/v1/megavault/historicalPnl", dydxIndexer) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := c.client.Do(req) if err != nil { - return nil, 0, err + return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, 0, fmt.Errorf("HTTP %d", resp.StatusCode) + return nil, fmt.Errorf("HTTP %d from %s", resp.StatusCode, url) } var result struct { - Fills []dydxFill `json:"fills"` + MegavaultPnl []megavaultPnlEntry `json:"megavaultPnl"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, 0, err - } - - var maxHeight uint64 - for _, f := range result.Fills { - h, _ := strconv.ParseUint(f.CreatedAtHeight, 10, 64) - if h > maxHeight { - maxHeight = h - } + return nil, fmt.Errorf("decode: %w", err) } - return result.Fills, maxHeight, nil + return result.MegavaultPnl, nil } -func classifyDyDXFill(f dydxFill) (component, beneficiary string) { - switch f.Liquidity { - case "TAKER": - return "taker_fee", "staker" // split handled by materializer via allocation_params - case "MAKER": - return "maker_fee", "lp" +// pnlDelta subtracts two integer-string USDC values and returns the delta as a string. +// Returns "" if either input is malformed. +func pnlDelta(prev, curr string) string { + p, err1 := strconv.ParseInt(prev, 10, 64) + c, err2 := strconv.ParseInt(curr, 10, 64) + if err1 != nil || err2 != nil { + return "" } - return "", "" + d := c - p + return strconv.FormatInt(d, 10) } -// toMicroUSDC converts a decimal USDC string (e.g. "1.234567") to integer micro-USDC -// without using float64 to avoid rounding errors in accounting code. -// dYdX indexer always returns exactly 6 decimal places. -func toMicroUSDC(s string) string { - if s == "" { - return "0" +func parseTime(s string) (time.Time, error) { + formats := []string{ + time.RFC3339Nano, + "2006-01-02T15:04:05.000Z", + "2006-01-02T15:04:05Z", } - dot := -1 - for i, c := range s { - if c == '.' { - dot = i - break + for _, f := range formats { + if t, err := time.Parse(f, s); err == nil { + return t, nil } } - if dot == -1 { - // No decimal point — multiply by 1e6 - return s + "000000" - } - intPart := s[:dot] - fracPart := s[dot+1:] - // Pad or truncate to exactly 6 decimal places - for len(fracPart) < 6 { - fracPart += "0" - } - fracPart = fracPart[:6] - // Remove leading zeros from intPart to avoid octal interpretation, then combine - result := intPart + fracPart - // Strip leading zeros (but keep at least one digit) - for len(result) > 1 && result[0] == '0' { - result = result[1:] - } - return result -} - -func trimNegative(s string) string { - if len(s) > 0 && s[0] == '-' { - return s[1:] - } - return s + return time.Time{}, fmt.Errorf("cannot parse time: %q", s) } From b6b869bb1d1e2e253eb37673175582cb2b60761c Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:20:53 +0200 Subject: [PATCH 14/39] feat: add Hyperliquid collector via DeFiLlama (perps/HLP/spot breakdown, 590 days) --- harnesses/apps/cmd/collector/main.go | 12 +- harnesses/apps/cmd/materializer/main.go | 2 +- .../apps/internal/collect/rest/hyperliquid.go | 154 ++++++++++++++++++ 3 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 harnesses/apps/internal/collect/rest/hyperliquid.go diff --git a/harnesses/apps/cmd/collector/main.go b/harnesses/apps/cmd/collector/main.go index 37b177f4..b98aab73 100644 --- a/harnesses/apps/cmd/collector/main.go +++ b/harnesses/apps/cmd/collector/main.go @@ -29,25 +29,27 @@ func main() { }() dydx := rest.NewDyDX() + hl := rest.NewHyperliquid() for { - if err := runDyDX(ctx, db, dydx); err != nil { + if err := runCollector(ctx, db, dydx, "dydx-v4:dydx-chain"); err != nil { log.Printf("dydx collector error: %v", err) } + if err := runCollector(ctx, db, hl, "hyperliquid:hypercore"); err != nil { + log.Printf("hyperliquid collector error: %v", err) + } time.Sleep(60 * time.Second) } } -func runDyDX(ctx context.Context, db *ledger.DB, col *rest.DyDXCollector) error { - const deploymentID = "dydx-v4:dydx-chain" - +func runCollector(ctx context.Context, db *ledger.DB, col spec.Collector, deploymentID string) error { from, err := db.GetCursor(ctx, deploymentID) if err != nil { return err } to := spec.Cursor{ - Height: ^uint64(0), // collect up to tip + Height: ^uint64(0), Ts: time.Now(), Finalized: true, } diff --git a/harnesses/apps/cmd/materializer/main.go b/harnesses/apps/cmd/materializer/main.go index f6134538..c88c677c 100644 --- a/harnesses/apps/cmd/materializer/main.go +++ b/harnesses/apps/cmd/materializer/main.go @@ -39,7 +39,7 @@ func main() { func runMaterialize(ctx context.Context, db *ledger.DB, checker *invariant.Checker) error { const mv = 1 - deployments := []string{"dydx-v4:dydx-chain"} + deployments := []string{"dydx-v4:dydx-chain", "hyperliquid:hypercore"} for _, dep := range deployments { if err := db.Materialize(ctx, dep, mv); err != nil { diff --git a/harnesses/apps/internal/collect/rest/hyperliquid.go b/harnesses/apps/internal/collect/rest/hyperliquid.go new file mode 100644 index 00000000..4bbaf207 --- /dev/null +++ b/harnesses/apps/internal/collect/rest/hyperliquid.go @@ -0,0 +1,154 @@ +package rest + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +const defilllamaHL = "https://api.llama.fi/summary/fees/hyperliquid" + +type HyperliquidCollector struct { + client *http.Client +} + +func NewHyperliquid() *HyperliquidCollector { + return &HyperliquidCollector{client: &http.Client{Timeout: 30 * time.Second}} +} + +func (c *HyperliquidCollector) Name() string { return "hyperliquid-defillama" } + +// hlDayBreakdown holds the per-category fees for one day as returned by DeFiLlama. +// Perps → taker_fee / burn (goes to AF which buys+burns HYPE) +// HLP → taker_fee / lp (HLP vault earns from market making) +// Spot → spot_fee / burn (spot orderbook fees, mostly AF) +type hlDayBreakdown struct { + Ts int64 // unix seconds (DeFiLlama bucket start) + Perps float64 // "Hyperliquid Perps" + HLP float64 // "Hyperliquid HLP" + Spot float64 // "Hyperliquid Spot Orderbook" +} + +func (c *HyperliquidCollector) Collect( + ctx context.Context, + deploymentID string, + from, to spec.Cursor, + out chan<- spec.FeeEvent, +) (spec.Cursor, error) { + cursor := from + + days, err := c.fetchBreakdown(ctx) + if err != nil { + return cursor, fmt.Errorf("hyperliquid: defillama: %w", err) + } + + for _, d := range days { + // Use unix timestamp (seconds) as synthetic height for cursor tracking. + h := uint64(d.Ts) + if h < from.Height { + continue + } + if h >= to.Height { + break + } + + ts := time.Unix(d.Ts, 0).UTC() + + // Skip days with zero fees (typically the incomplete current day at the tail). + if d.Perps == 0 && d.HLP == 0 && d.Spot == 0 { + continue + } + + // DeFiLlama returns whole-dollar amounts. + // Store as raw USD integer strings with Decimals=0. + emit := func(component, beneficiary string, amount float64) { + if amount <= 0 { + return + } + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("hl:defillama:%d:%s:%s", d.Ts, component, beneficiary), + Ts: ts, + Height: h, + Component: component, + Beneficiary: beneficiary, + Token: "USD", + AmountRaw: fmt.Sprintf("%d", int64(amount)), + Decimals: 0, + Market: "all", + Finality: spec.FinalityFinal, + Source: "defillama-hyperliquid", + Meta: map[string]string{ + "note": "Phase 1 proxy via DeFiLlama. Will be replaced by S3 fills in Phase 2.", + }, + } + } + + emit("taker_fee", "burn", d.Perps) + emit("taker_fee", "lp", d.HLP) + emit("spot_fee", "burn", d.Spot) + + cursor = spec.Cursor{Height: h + 86400, Ts: ts, Finalized: true} + } + + return cursor, nil +} + +func (c *HyperliquidCollector) fetchBreakdown(ctx context.Context) ([]hlDayBreakdown, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, defilllamaHL, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", "ocb-apps/1.0") + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP %d from %s", resp.StatusCode, defilllamaHL) + } + + var result struct { + TotalDataChartBreakdown [][]json.RawMessage `json:"totalDataChartBreakdown"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("decode: %w", err) + } + + out := make([]hlDayBreakdown, 0, len(result.TotalDataChartBreakdown)) + for _, entry := range result.TotalDataChartBreakdown { + if len(entry) < 2 { + continue + } + var ts int64 + if err := json.Unmarshal(entry[0], &ts); err != nil { + continue + } + + // breakdown shape: {"Hyperliquid L1": {"Hyperliquid Perps": N, "Hyperliquid HLP": N, ...}} + var outer map[string]map[string]float64 + if err := json.Unmarshal(entry[1], &outer); err != nil { + continue + } + inner, ok := outer["Hyperliquid L1"] + if !ok { + continue + } + + out = append(out, hlDayBreakdown{ + Ts: ts, + Perps: inner["Hyperliquid Perps"], + HLP: inner["Hyperliquid HLP"], + Spot: inner["Hyperliquid Spot Orderbook"], + }) + } + + return out, nil +} From 9bc9f69fba322ee97eeb965c275d24207371edd9 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:54:51 +0200 Subject: [PATCH 15/39] feat: /apps leaderboard page + Go API service (Hyperliquid $45M/30d, dYdX $25K/30d) --- harnesses/apps/Dockerfile | 2 + harnesses/apps/cmd/api/main.go | 174 ++++++++++++++++++++++ src/app/apps/page.tsx | 57 +++++++ src/components/apps-leaderboard-table.tsx | 114 ++++++++++++++ src/lib/apps-leaderboard.ts | 34 +++++ 5 files changed, 381 insertions(+) create mode 100644 harnesses/apps/cmd/api/main.go create mode 100644 src/app/apps/page.tsx create mode 100644 src/components/apps-leaderboard-table.tsx create mode 100644 src/lib/apps-leaderboard.ts diff --git a/harnesses/apps/Dockerfile b/harnesses/apps/Dockerfile index 4058acc2..a3752b65 100644 --- a/harnesses/apps/Dockerfile +++ b/harnesses/apps/Dockerfile @@ -5,9 +5,11 @@ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /app/collector ./cmd/collector RUN CGO_ENABLED=0 go build -o /app/materializer ./cmd/materializer +RUN CGO_ENABLED=0 go build -o /app/api ./cmd/api FROM alpine:3.20 RUN apk add --no-cache ca-certificates tzdata COPY --from=builder /app/collector /app/collector COPY --from=builder /app/materializer /app/materializer +COPY --from=builder /app/api /app/api COPY migrations/ /app/migrations/ diff --git a/harnesses/apps/cmd/api/main.go b/harnesses/apps/cmd/api/main.go new file mode 100644 index 00000000..bd5793ce --- /dev/null +++ b/harnesses/apps/cmd/api/main.go @@ -0,0 +1,174 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "net/http" + "os" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +func main() { + ctx := context.Background() + pool, err := pgxpool.New(ctx, mustEnv("DATABASE_URL")) + if err != nil { + log.Fatalf("apps-api: connect: %v", err) + } + defer pool.Close() + + mux := http.NewServeMux() + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"ok"}`)) + }) + mux.HandleFunc("/api/leaderboard", corsJSON(handleLeaderboard(pool))) + + addr := ":2115" + log.Printf("apps-api listening on %s", addr) + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatalf("apps-api: serve: %v", err) + } +} + +type WindowMetrics struct { + Gross float64 `json:"gross"` + Burn float64 `json:"burn"` + LP float64 `json:"lp"` + Spot float64 `json:"spot"` +} + +type ProtocolRow struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Category string `json:"category"` + LatestDay string `json:"latestDay"` + Windows map[string]WindowMetrics `json:"windows"` +} + +type LeaderboardResponse struct { + UpdatedAt string `json:"updatedAt"` + Protocols []ProtocolRow `json:"protocols"` +} + +var deploymentMeta = map[string]struct{ Name, Slug, Category string }{ + "hyperliquid:hypercore": {Name: "Hyperliquid", Slug: "hyperliquid", Category: "perps"}, + "dydx-v4:dydx-chain": {Name: "dYdX", Slug: "dydx", Category: "perps"}, +} + +func handleLeaderboard(pool *pgxpool.Pool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + rows, err := pool.Query(ctx, ` + SELECT + deployment_id, + beneficiary, + component, + SUM(CASE WHEN bucket_start >= now() - INTERVAL '24 hours' THEN amount_usd ELSE 0 END) AS h24, + SUM(CASE WHEN bucket_start >= now() - INTERVAL '7 days' THEN amount_usd ELSE 0 END) AS d7, + SUM(CASE WHEN bucket_start >= now() - INTERVAL '30 days' THEN amount_usd ELSE 0 END) AS d30, + SUM(amount_usd) AS all_time, + MAX(bucket_start)::text AS latest_day + FROM fee_facts + WHERE methodology_version = 1 + GROUP BY deployment_id, beneficiary, component + ORDER BY deployment_id, beneficiary, component`, + ) + if err != nil { + log.Printf("leaderboard query: %v", err) + http.Error(w, "internal", http.StatusInternalServerError) + return + } + defer rows.Close() + + type key struct{ id, beneficiary, component string } + type vals struct{ h24, d7, d30, allTime float64; latestDay string } + data := map[key]vals{} + + for rows.Next() { + var k key + var v vals + if err := rows.Scan(&k.id, &k.beneficiary, &k.component, &v.h24, &v.d7, &v.d30, &v.allTime, &v.latestDay); err != nil { + continue + } + data[k] = v + } + + // Aggregate per deployment into WindowMetrics. + depMap := map[string]*ProtocolRow{} + for k, v := range data { + row, ok := depMap[k.id] + if !ok { + meta := deploymentMeta[k.id] + row = &ProtocolRow{ + ID: k.id, + Name: meta.Name, + Slug: meta.Slug, + Category: meta.Category, + Windows: map[string]WindowMetrics{}, + } + depMap[k.id] = row + } + if v.latestDay > row.LatestDay { + row.LatestDay = v.latestDay + } + addToWindow := func(winKey string, amount float64) { + wm := row.Windows[winKey] + wm.Gross += amount + switch k.beneficiary { + case "burn": + wm.Burn += amount + case "lp": + wm.LP += amount + } + if k.component == "spot_fee" { + wm.Spot += amount + } + row.Windows[winKey] = wm + } + addToWindow("24h", v.h24) + addToWindow("7d", v.d7) + addToWindow("30d", v.d30) + addToWindow("allTime", v.allTime) + } + + protocols := make([]ProtocolRow, 0, len(depMap)) + for _, row := range depMap { + protocols = append(protocols, *row) + } + // Sort by 30d gross descending. + for i := 1; i < len(protocols); i++ { + for j := i; j > 0 && protocols[j].Windows["30d"].Gross > protocols[j-1].Windows["30d"].Gross; j-- { + protocols[j], protocols[j-1] = protocols[j-1], protocols[j] + } + } + + resp := LeaderboardResponse{ + UpdatedAt: time.Now().UTC().Format(time.RFC3339), + Protocols: protocols, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + } +} + +func corsJSON(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Cache-Control", "public, max-age=60, stale-while-revalidate=300") + h(w, r) + } +} + +func mustEnv(key string) string { + v := os.Getenv(key) + if v == "" { + log.Fatalf("missing env: %s", key) + } + return v +} diff --git a/src/app/apps/page.tsx b/src/app/apps/page.tsx new file mode 100644 index 00000000..70ceda2b --- /dev/null +++ b/src/app/apps/page.tsx @@ -0,0 +1,57 @@ +import type { Metadata } from "next"; +import { pageMetadata } from "@/lib/page-metadata"; +import { safeJsonLd, buildBreadcrumbJsonLd } from "@/lib/jsonld"; +import { SITE } from "@/data/site"; +import { fetchAppsLeaderboard, type ProtocolRow } from "@/lib/apps-leaderboard"; +import { AppsLeaderboardTable } from "@/components/apps-leaderboard-table"; + +const DESCRIPTION = + "Protocol revenue leaderboard for on-chain apps: fees captured by treasury, token holders, and LPs. Reproducible methodology, refreshed daily, sources public."; + +export const metadata: Metadata = pageMetadata({ + path: "/apps", + title: "Protocol Revenue Leaderboard 2026 — OpenChainBench", + description: DESCRIPTION, +}); + +export const revalidate = 300; + +export default async function AppsHubPage() { + const data = await fetchAppsLeaderboard(); + + const breadcrumb = { + "@context": "https://schema.org", + ...buildBreadcrumbJsonLd([ + { name: "Home", item: SITE.url }, + { name: "Apps", item: `${SITE.url}/apps` }, + ]), + }; + + return ( +
+