From 1c4af0f61cde09f75df47631fb84ef9a9aef4da1 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:18:58 +0200 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20codex=20auth=20chain=20=E2=80=94=20u?= =?UTF-8?q?n.defined.fi=20+=20utls=20+=20sidecar=20fallback=20(#1819)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: remove dead providers (BlastAPI shutdown, Ankr key-gated, dRPC/1RPC not supported) * fix: remove dead providers (BlastAPI shutdown, Ankr key-gated, dRPC/1RPC not supported) (#1789) * feat: add wave 7 chains to /chains hub (fantom, kusama, hydration, zetachain, haqq, etherlink, chiliz, wemix, songbird, etc-classic, telos, pulsechain, warden, oraichain, peaq) * 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) * fix: remove drift and variational from perp venue meta (404 on /perp redirect) * fix: filter non-provider result links in answers, add bridge to footer * fix: drop dead RPC providers across 14 chains (DNS failures, 404s, paid-plan blocks) * fix: hydration draft+placeholder provider, zetachain/songbird em-dash in findings * fix: hydration draft+placeholder provider, zetachain/songbird em-dash in findings (#1794) * feat: add /apps leaderboard foundation — specs, Go harness, Postgres schema, alerting * fix: interface ctx type, float-free toMicroUSDC, goroutine error capture, pagination cursor * feat: /apps leaderboard foundation (#1797) * feat: add /apps leaderboard foundation — specs, Go harness, Postgres schema, alerting * fix: interface ctx type, float-free toMicroUSDC, goroutine error capture, pagination cursor * fix: dydx collector json key megavaultPnl, cumulative-to-delta PnL, Decimals=0 * feat: add Hyperliquid collector via DeFiLlama (perps/HLP/spot breakdown, 590 days) * feat: /apps leaderboard page + Go API service (Hyperliquid $45M/30d, dYdX $25K/30d) * feat(apps): first-party data sources — HL node fills + GMX Subsquid (#1801) * feat(apps): switch HL collector to node fills, add hl-fills-agg Replace DeFiLlama proxy with first-party Hyperliquid node fills. - Reads fee/builderFee from node_fills_by_block on SGP (15.235.224.14) - hl-fills-agg.py aggregates daily totals, runs as pm2 on SGP:2116 - net_usdc = gross - builder = amount flowing to AF + HLP + deployers - Fills archive starts 2026-06-01, backfill runs automatically * feat(apps): add GMX v2 collector (Arbitrum + Avalanche) via Subsquid First-party data from GMX-operated Subsquid GraphQL endpoint. Position fees (63% LP / 37% protocol) + swap fees split per governance. Borrowing fees classified as LP revenue (pool-only, no protocol cut). Amounts in GMX internal 1e30 precision, stored with Decimals=30. * fix: dYdX collector — volume×fee-rate instead of MegaVault PnL * feat: add Gains.trade collector (The Graph, needs THEGRAPH_API_KEY) * feat: Gains.trade collector via DeFiLlama public API (no key needed) * feat: gains-trade real-time WS collector via gTrade backend + eth_getTransactionReceipt * fix: leaderboard UI polish + map treasury->burn bucket for Gains.trade * feat: add logos + /perp links to apps leaderboard table * chore: remove defillama gains collector, WS handles gains going forward * fix: update codex/defined auth to use defined-attestation-token + CODEX_JWT bypass * feat: add Drift, Jupiter Perps, Vertex to apps leaderboard via DeFiLlama (#1803) - Generic DeFiLlama collector (defillama_generic.go): daily fees + revenue split into treasury/lp events - Correct DeFiLlama slugs: drift-trade, jupiter-perpetual-exchange, vertex-perps - Fix h24 window to 2 days so daily-bucket protocols show data (was missing for HL/GMX/dYdX) - Add new deployments to materializer and API meta - Frontend VENUE map updated with logos for all 3 new protocols * fix: dYdX fee rate 5bps → 3.5bps (blended effective rate) (#1804) * feat: add Drift, Jupiter Perps, Vertex to apps leaderboard via DeFiLlama - Generic DeFiLlama collector (defillama_generic.go): daily fees + revenue split into treasury/lp events - Correct DeFiLlama slugs: drift-trade, jupiter-perpetual-exchange, vertex-perps - Fix h24 window to 2 days so daily-bucket protocols show data (was missing for HL/GMX/dYdX) - Add new deployments to materializer and API meta - Frontend VENUE map updated with logos for all 3 new protocols * fix: dYdX fee rate 5bps -> 3.5bps (blended effective rate) * fix: dYdX full history — 90d limit → 1000d (Nov 2023 launch) (#1805) * feat: add Drift, Jupiter Perps, Vertex to apps leaderboard via DeFiLlama - Generic DeFiLlama collector (defillama_generic.go): daily fees + revenue split into treasury/lp events - Correct DeFiLlama slugs: drift-trade, jupiter-perpetual-exchange, vertex-perps - Fix h24 window to 2 days so daily-bucket protocols show data (was missing for HL/GMX/dYdX) - Add new deployments to materializer and API meta - Frontend VENUE map updated with logos for all 3 new protocols * fix: dYdX fee rate 5bps -> 3.5bps (blended effective rate) * fix: dYdX full history backfill — limit 90d -> 1000d (covers Nov 2023 launch) * perf: use recording rules for lz/bridge histogram queries, drop raw rate([24h]) * fix: truncate tron/token-quote methodology strings, telos em-dash in findings * perf: recording rules for hyperlane histogram queries * revert: layerzero back to raw queries * fix(agg-head-lag): update Codex mint to new /api/codex/token endpoint www.defined.fi/api (createApiTokens GraphQL mutation) is dead as of 2026-08. New endpoint is /api/codex/token which returns JWE directly. Still blocked by Vercel TLS checkpoint for Go HTTP — use CODEX_JWT env var or DEFINED_TOKEN_SERVICE_URL sidecar (Paris box) for automatic refresh. scrape-cookie: add awaitPromise fix, diagnostic headers, in-browser fetch test. * revert: layerzero back to raw queries (lz rate24h rule too expensive for VPS) * fix: remove em dashes in kaia-rpc and aurora-rpc findings * fix(agg-head-lag): update Codex mint to new /api/codex/token endpoint www.defined.fi/api (createApiTokens GraphQL mutation) is dead as of 2026-08. New endpoint is /api/codex/token which returns JWE directly. Still blocked by Vercel TLS checkpoint for Go HTTP — use CODEX_JWT env var or DEFINED_TOKEN_SERVICE_URL sidecar (Paris box) for automatic refresh. scrape-cookie: add awaitPromise fix, diagnostic headers, in-browser fetch test. * revert: layerzero back to raw queries (lz rate24h rule too expensive for VPS) * fix: agg head-lag uses DEFINED_TOKEN_SERVICE_URL sidecar for Codex JWE * feat: add solana-exec harness (pump.fun passive collector) + /apps/exec page * fix: materializer includes current hour, collector skips failed sigs, api 24h window correct * fix: remove em dashes in kaia-rpc and aurora-rpc findings * feat: add solana-exec harness + /apps/exec page (#1811) * fix(agg-head-lag): update Codex mint to new /api/codex/token endpoint www.defined.fi/api (createApiTokens GraphQL mutation) is dead as of 2026-08. New endpoint is /api/codex/token which returns JWE directly. Still blocked by Vercel TLS checkpoint for Go HTTP — use CODEX_JWT env var or DEFINED_TOKEN_SERVICE_URL sidecar (Paris box) for automatic refresh. scrape-cookie: add awaitPromise fix, diagnostic headers, in-browser fetch test. * revert: layerzero back to raw queries (lz rate24h rule too expensive for VPS) * fix: agg head-lag uses DEFINED_TOKEN_SERVICE_URL sidecar for Codex JWE * feat: add solana-exec harness (pump.fun passive collector) + /apps/exec page * fix: materializer includes current hour, collector skips failed sigs, api 24h window correct * fix: remove em dashes in kaia-rpc and aurora-rpc findings * fix(codex-ws): direct dialer + local JWE mint to fix 4403 IP mismatch * fix: weighted avg for fee/jito metrics across hourly buckets * fix: log warning when solana-exec collector hits sig limit (#1812) * fix(agg-head-lag): update Codex mint to new /api/codex/token endpoint www.defined.fi/api (createApiTokens GraphQL mutation) is dead as of 2026-08. New endpoint is /api/codex/token which returns JWE directly. Still blocked by Vercel TLS checkpoint for Go HTTP — use CODEX_JWT env var or DEFINED_TOKEN_SERVICE_URL sidecar (Paris box) for automatic refresh. scrape-cookie: add awaitPromise fix, diagnostic headers, in-browser fetch test. * revert: layerzero back to raw queries (lz rate24h rule too expensive for VPS) * fix: agg head-lag uses DEFINED_TOKEN_SERVICE_URL sidecar for Codex JWE * feat: add solana-exec harness (pump.fun passive collector) + /apps/exec page * fix: materializer includes current hour, collector skips failed sigs, api 24h window correct * fix: remove em dashes in kaia-rpc and aurora-rpc findings * fix(codex-ws): direct dialer + local JWE mint to fix 4403 IP mismatch * fix: weighted avg for fee/jito metrics across hourly buckets * fix(codex-ws): resolve merge conflict in defined_auth * fix: log warning when sig limit hit on incremental poll * feat: paginate getSignaturesForAddress for complete tx coverage (#1813) * fix: remove dead RPC providers, allow optional p90/p99 for gauge benches * feat: paginate getSignaturesForAddress for complete tx coverage * fix(codex-ws): in-process chromedp scraper for fresh JWE every 5min * fix: remove dead providers from xdc/etc-rpc, allow optional p90/p99 in load.ts * remove: delete indexing-freshness bench * feat: paginate getSignaturesForAddress for complete tx coverage * fix(codex-ws): in-process chromedp scraper for fresh JWE every 5min * fix: remove dead providers from xdc/etc-rpc, allow optional p90/p99 in load.ts * feat(solana-exec): accurate tx volume via raw sig pagination + 100-sig enhanced cap * remove: delete indexing-freshness bench (no data) * remove: delete indexing-freshness bench * feat: paginate getSignaturesForAddress for complete tx coverage * fix(codex-ws): in-process chromedp scraper for fresh JWE every 5min * fix: remove dead providers from xdc/etc-rpc, allow optional p90/p99 in load.ts * feat(solana-exec): accurate tx volume via raw sig pagination + 100-sig enhanced cap * remove: delete indexing-freshness bench (no data) * fix(codex): utls scraper + GitHub Actions token push (#1818) * fix(codex): utls scraper + GitHub Actions token push * fix: use un.defined.fi legacy API for codex JWE minting un.defined.fi/api createApiTokens still returns valid JWE tokens without requiring session cookies or CSRF. Try this first via utls, fall back to www.defined.fi/api/codex/token (needs page visit + CSRF). generateDefinedJWTToken also switched to un.defined.fi. --- .github/workflows/codex-push.yml | 44 +++ 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 ++++++++++++ benchmarks/astar-rpc.yml | 60 +--- benchmarks/aurora-rpc.yml | 61 +--- benchmarks/bridge-quote-latency.yml | 128 ++++---- benchmarks/chiliz-rpc.yml | 23 -- benchmarks/ethereum-classic-rpc.yml | 60 +--- benchmarks/ethereum-rpc.yml | 42 +-- benchmarks/etherlink-rpc.yml | 46 --- benchmarks/fantom-rpc.yml | 46 --- benchmarks/haqq-rpc.yml | 23 -- benchmarks/hydration-rpc.yml | 122 +------ benchmarks/hyperlane-message-latency.yml | 190 +++++------ benchmarks/indexing-freshness.yml | 127 -------- benchmarks/kaia-rpc.yml | 38 +-- benchmarks/kusama-rpc.yml | 69 ---- benchmarks/moonriver-rpc.yml | 38 +-- benchmarks/oraichain-rpc.yml | 23 -- benchmarks/peaq-rpc.yml | 23 -- benchmarks/ronin-rpc.yml | 39 +-- benchmarks/sei-rpc.yml | 39 +-- benchmarks/songbird-rpc.yml | 84 +---- benchmarks/telos-rpc.yml | 62 +--- benchmarks/token-quote-coverage.yml | 2 +- benchmarks/tron-rpc.yml | 47 +-- benchmarks/warden-rpc.yml | 37 +-- benchmarks/wemix-rpc.yml | 46 --- benchmarks/xdc-rpc.yml | 83 +---- benchmarks/zetachain-rpc.yml | 107 +------ harnesses/aggregator-head-lag/Dockerfile | 5 +- .../cmd/scrape-cookie/main.go | 150 +++++++++ .../cmd/script/codex_scraper.go | 162 ++++++++++ .../cmd/script/defined_auth.go | 159 +++++++-- .../cmd/script/head_lag_monitor.go | 21 +- .../aggregator-head-lag/cmd/script/main.go | 5 + .../cmd/script/scrape_session.go | 4 +- .../cmd/script/utls_codex.go | 223 +++++++++++++ .../aggregator-head-lag/cmd/test-utls/main.go | 261 +++++++++++++++ harnesses/aggregator-head-lag/go.mod | 4 + harnesses/aggregator-head-lag/go.sum | 6 + .../cmd/script/defined_auth.go | 9 +- .../cmd/script/scrape_session.go | 2 +- harnesses/apps/Dockerfile | 15 + harnesses/apps/cmd/api/main.go | 180 +++++++++++ harnesses/apps/cmd/collector/main.go | 141 ++++++++ harnesses/apps/cmd/materializer/main.go | 77 +++++ harnesses/apps/docker-compose.apps.yml | 65 ++++ harnesses/apps/env.apps.example | 32 ++ harnesses/apps/go.mod | 27 ++ harnesses/apps/go.sum | 54 ++++ .../collect/rest/defillama_generic.go | 189 +++++++++++ harnesses/apps/internal/collect/rest/dydx.go | 188 +++++++++++ .../apps/internal/collect/rest/gains_trade.go | 181 +++++++++++ .../internal/collect/rest/gains_trade_ws.go | 232 ++++++++++++++ .../apps/internal/collect/rest/gmx_v2.go | 301 ++++++++++++++++++ .../apps/internal/collect/rest/hyperliquid.go | 144 +++++++++ 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 +++++++++ harnesses/apps/scripts/hl-fills-agg.py | 156 +++++++++ .../cmd/script/defined_auth.go | 9 +- .../cmd/script/scrape_session.go | 2 +- .../network-coverage/cmd/script/codex.go | 7 +- .../pm-rate-limits/cmd/script/codex_auth.go | 7 +- .../rpc-capabilities/cmd/script/config.go | 161 ++++------ harnesses/solana-exec/Dockerfile | 15 + harnesses/solana-exec/cmd/api/main.go | 202 ++++++++++++ harnesses/solana-exec/cmd/collector/main.go | 181 +++++++++++ .../solana-exec/cmd/materializer/main.go | 40 +++ .../docker-compose.solana-exec.yml | 46 +++ harnesses/solana-exec/env.solana-exec.example | 10 + harnesses/solana-exec/go.mod | 14 + harnesses/solana-exec/go.sum | 28 ++ .../solana-exec/internal/helius/client.go | 146 +++++++++ .../internal/platform/platforms.go | 24 ++ harnesses/solana-exec/internal/store/store.go | 165 ++++++++++ harnesses/solana-exec/migrations/001_init.sql | 49 +++ .../solana-exec/migrations/002_raw_counts.sql | 18 ++ src/app/answers/[slug]/page.tsx | 7 + src/app/apps/exec/page.tsx | 77 +++++ src/app/apps/page.tsx | 57 ++++ src/components/apps-leaderboard-table.tsx | 172 ++++++++++ src/components/site-footer.tsx | 1 + src/components/solana-exec-table.tsx | 156 +++++++++ src/lib/apps-leaderboard.ts | 34 ++ src/lib/chains.ts | 135 ++++++++ src/lib/materialize/load.ts | 5 + src/lib/perp-venue-context.ts | 2 - src/lib/solana-exec.ts | 54 ++++ 98 files changed, 6834 insertions(+), 1534 deletions(-) create mode 100644 .github/workflows/codex-push.yml 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 delete mode 100644 benchmarks/indexing-freshness.yml create mode 100644 harnesses/aggregator-head-lag/cmd/scrape-cookie/main.go create mode 100644 harnesses/aggregator-head-lag/cmd/script/codex_scraper.go create mode 100644 harnesses/aggregator-head-lag/cmd/script/utls_codex.go create mode 100644 harnesses/aggregator-head-lag/cmd/test-utls/main.go create mode 100644 harnesses/apps/Dockerfile create mode 100644 harnesses/apps/cmd/api/main.go 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/defillama_generic.go create mode 100644 harnesses/apps/internal/collect/rest/dydx.go create mode 100644 harnesses/apps/internal/collect/rest/gains_trade.go create mode 100644 harnesses/apps/internal/collect/rest/gains_trade_ws.go create mode 100644 harnesses/apps/internal/collect/rest/gmx_v2.go create mode 100644 harnesses/apps/internal/collect/rest/hyperliquid.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 create mode 100644 harnesses/apps/scripts/hl-fills-agg.py create mode 100644 harnesses/solana-exec/Dockerfile create mode 100644 harnesses/solana-exec/cmd/api/main.go create mode 100644 harnesses/solana-exec/cmd/collector/main.go create mode 100644 harnesses/solana-exec/cmd/materializer/main.go create mode 100644 harnesses/solana-exec/docker-compose.solana-exec.yml create mode 100644 harnesses/solana-exec/env.solana-exec.example create mode 100644 harnesses/solana-exec/go.mod create mode 100644 harnesses/solana-exec/go.sum create mode 100644 harnesses/solana-exec/internal/helius/client.go create mode 100644 harnesses/solana-exec/internal/platform/platforms.go create mode 100644 harnesses/solana-exec/internal/store/store.go create mode 100644 harnesses/solana-exec/migrations/001_init.sql create mode 100644 harnesses/solana-exec/migrations/002_raw_counts.sql create mode 100644 src/app/apps/exec/page.tsx create mode 100644 src/app/apps/page.tsx create mode 100644 src/components/apps-leaderboard-table.tsx create mode 100644 src/components/solana-exec-table.tsx create mode 100644 src/lib/apps-leaderboard.ts create mode 100644 src/lib/solana-exec.ts diff --git a/.github/workflows/codex-push.yml b/.github/workflows/codex-push.yml new file mode 100644 index 00000000..bdefd9a0 --- /dev/null +++ b/.github/workflows/codex-push.yml @@ -0,0 +1,44 @@ +name: Codex token push + +on: + schedule: + - cron: '*/5 * * * *' + workflow_dispatch: + +jobs: + push-token: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: harnesses/aggregator-head-lag/go.mod + cache-dependency-path: harnesses/aggregator-head-lag/go.sum + + - name: Build scraper + working-directory: harnesses/aggregator-head-lag + run: go build -o /tmp/codex-scraper ./cmd/test-utls/ + + - name: Scrape and push token + run: | + set -euo pipefail + output=$(MODE=default /tmp/codex-scraper 2>&1) + echo "$output" | head -5 + token=$(echo "$output" | grep '^CODEX_TOKEN=' | cut -d= -f2-) + if [ -z "$token" ] || [ ${#token} -lt 100 ]; then + echo "ERROR: no token in output" + echo "$output" + exit 1 + fi + echo "Got token (len=${#token}), pushing to sidecar..." + http_code=$(curl -s -o /tmp/push_resp -w "%{http_code}" -X POST \ + -H "Content-Type: text/plain" \ + --data-raw "$token" \ + "http://57.130.19.92:8080/push") + if [ "$http_code" = "204" ]; then + echo "Push OK" + else + echo "Push failed (HTTP $http_code): $(cat /tmp/push_resp)" + exit 1 + fi 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/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..ffb33df7 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/bridge-quote-latency.yml b/benchmarks/bridge-quote-latency.yml index a26569f1..7362a22e 100644 --- a/benchmarks/bridge-quote-latency.yml +++ b/benchmarks/bridge-quote-latency.yml @@ -112,7 +112,7 @@ faq: # rank_matrix_query required by spec validator when dimensions.region is declared. # label_replace maps bridge= → provider= so badge scoping works per region. -rank_matrix_query: histogram_quantile(0.50, sum by (provider, region, le) (label_replace(rate(bridge_quote_latency_ms_bucket[24h]), "provider", "$1", "bridge", "(.+)"))) +rank_matrix_query: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h_provider) providers: - slug: mobula @@ -120,22 +120,22 @@ providers: tag: Aggregator + intent layer formula: "Median wall-clock ms over 24h for Mobula's quote API to return a usable USDC route, sampled every 5 minutes across 4 routes and 3 notionals from 3 regions (eu-west, us-east, sgp)." queries: - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mobula"}[24h]))) - p90: histogram_quantile(0.90, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mobula"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mobula"}[24h]))) - mean: sum(rate(bridge_quote_latency_ms_sum{bridge="mobula"}[24h])) / sum(rate(bridge_quote_latency_ms_count{bridge="mobula"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="mobula"}[24h])) - sample_size: sum(increase(bridge_quote_latency_ms_count{bridge="mobula"}[24h])) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="mobula"}) + p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="mobula"}) + p99: histogram_quantile(0.99, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="mobula"}) + mean: ocb:bridge_quote_latency_ms_sum:rate24h{bridge="mobula"} / ocb:bridge_quote_latency_ms_count:rate24h{bridge="mobula"} + success: ocb:bridge_quote_success:avg24h{bridge="mobula"} + sample_size: ocb:bridge_quote_latency_ms_count:increase24h{bridge="mobula"} series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mobula"}[1h]))) regions: - region: eu-west - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mobula", region="eu-west"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="mobula", region="eu-west"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mobula", region="eu-west"}[1h]))) - region: us-east - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mobula", region="us-east"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="mobula", region="us-east"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mobula", region="us-east"}[1h]))) - region: sgp - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mobula", region="sgp"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="mobula", region="sgp"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="mobula", region="sgp"}[1h]))) - slug: relay @@ -143,22 +143,22 @@ providers: tag: Cross-chain relay formula: "Median wall-clock ms over 24h for Relay's solver-quote API to return a usable USDC route, sampled every 5 minutes across 4 routes and 3 notionals from 3 regions (eu-west, us-east, sgp)." queries: - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="relay"}[24h]))) - p90: histogram_quantile(0.90, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="relay"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="relay"}[24h]))) - mean: sum(rate(bridge_quote_latency_ms_sum{bridge="relay"}[24h])) / sum(rate(bridge_quote_latency_ms_count{bridge="relay"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="relay"}[24h])) - sample_size: sum(increase(bridge_quote_latency_ms_count{bridge="relay"}[24h])) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="relay"}) + p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="relay"}) + p99: histogram_quantile(0.99, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="relay"}) + mean: ocb:bridge_quote_latency_ms_sum:rate24h{bridge="relay"} / ocb:bridge_quote_latency_ms_count:rate24h{bridge="relay"} + success: ocb:bridge_quote_success:avg24h{bridge="relay"} + sample_size: ocb:bridge_quote_latency_ms_count:increase24h{bridge="relay"} series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="relay"}[1h]))) regions: - region: eu-west - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="relay", region="eu-west"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="relay", region="eu-west"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="relay", region="eu-west"}[1h]))) - region: us-east - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="relay", region="us-east"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="relay", region="us-east"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="relay", region="us-east"}[1h]))) - region: sgp - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="relay", region="sgp"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="relay", region="sgp"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="relay", region="sgp"}[1h]))) - slug: lifi @@ -166,22 +166,22 @@ providers: tag: Aggregator formula: "Median wall-clock ms over 24h for LI.FI's route-search API to return a usable USDC route across underlying bridges, sampled every 5 minutes across 4 routes and 3 notionals from 3 regions (eu-west, us-east, sgp)." queries: - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="lifi"}[24h]))) - p90: histogram_quantile(0.90, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="lifi"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="lifi"}[24h]))) - mean: sum(rate(bridge_quote_latency_ms_sum{bridge="lifi"}[24h])) / sum(rate(bridge_quote_latency_ms_count{bridge="lifi"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="lifi"}[24h])) - sample_size: sum(increase(bridge_quote_latency_ms_count{bridge="lifi"}[24h])) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="lifi"}) + p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="lifi"}) + p99: histogram_quantile(0.99, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="lifi"}) + mean: ocb:bridge_quote_latency_ms_sum:rate24h{bridge="lifi"} / ocb:bridge_quote_latency_ms_count:rate24h{bridge="lifi"} + success: ocb:bridge_quote_success:avg24h{bridge="lifi"} + sample_size: ocb:bridge_quote_latency_ms_count:increase24h{bridge="lifi"} series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="lifi"}[1h]))) regions: - region: eu-west - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="lifi", region="eu-west"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="lifi", region="eu-west"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="lifi", region="eu-west"}[1h]))) - region: us-east - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="lifi", region="us-east"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="lifi", region="us-east"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="lifi", region="us-east"}[1h]))) - region: sgp - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="lifi", region="sgp"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="lifi", region="sgp"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="lifi", region="sgp"}[1h]))) - slug: debridge @@ -189,22 +189,22 @@ providers: tag: Native-token fees formula: "Median wall-clock ms over 24h for deBridge's direct-protocol quote API to return a single-route USDC quote, sampled every 5 minutes across 4 routes and 3 notionals from 3 regions (eu-west, us-east, sgp)." queries: - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="debridge"}[24h]))) - p90: histogram_quantile(0.90, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="debridge"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="debridge"}[24h]))) - mean: sum(rate(bridge_quote_latency_ms_sum{bridge="debridge"}[24h])) / sum(rate(bridge_quote_latency_ms_count{bridge="debridge"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="debridge"}[24h])) - sample_size: sum(increase(bridge_quote_latency_ms_count{bridge="debridge"}[24h])) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="debridge"}) + p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="debridge"}) + p99: histogram_quantile(0.99, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="debridge"}) + mean: ocb:bridge_quote_latency_ms_sum:rate24h{bridge="debridge"} / ocb:bridge_quote_latency_ms_count:rate24h{bridge="debridge"} + success: ocb:bridge_quote_success:avg24h{bridge="debridge"} + sample_size: ocb:bridge_quote_latency_ms_count:increase24h{bridge="debridge"} series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="debridge"}[1h]))) regions: - region: eu-west - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="debridge", region="eu-west"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="debridge", region="eu-west"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="debridge", region="eu-west"}[1h]))) - region: us-east - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="debridge", region="us-east"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="debridge", region="us-east"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="debridge", region="us-east"}[1h]))) - region: sgp - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="debridge", region="sgp"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="debridge", region="sgp"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="debridge", region="sgp"}[1h]))) - slug: near-intents @@ -212,22 +212,22 @@ providers: tag: Intent layer (NEAR) formula: "Median wall-clock ms over 24h for Near Intents 1Click API to return a usable USDC quote via the solver auction bus, sampled every 5 minutes across the supported routes and 3 notionals from 3 regions (eu-west, us-east, sgp)." queries: - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="near-intents"}[24h]))) - p90: histogram_quantile(0.90, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="near-intents"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="near-intents"}[24h]))) - mean: sum(rate(bridge_quote_latency_ms_sum{bridge="near-intents"}[24h])) / sum(rate(bridge_quote_latency_ms_count{bridge="near-intents"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="near-intents"}[24h])) - sample_size: sum(increase(bridge_quote_latency_ms_count{bridge="near-intents"}[24h])) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="near-intents"}) + p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="near-intents"}) + p99: histogram_quantile(0.99, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="near-intents"}) + mean: ocb:bridge_quote_latency_ms_sum:rate24h{bridge="near-intents"} / ocb:bridge_quote_latency_ms_count:rate24h{bridge="near-intents"} + success: ocb:bridge_quote_success:avg24h{bridge="near-intents"} + sample_size: ocb:bridge_quote_latency_ms_count:increase24h{bridge="near-intents"} series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="near-intents"}[1h]))) regions: - region: eu-west - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="near-intents", region="eu-west"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="near-intents", region="eu-west"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="near-intents", region="eu-west"}[1h]))) - region: us-east - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="near-intents", region="us-east"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="near-intents", region="us-east"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="near-intents", region="us-east"}[1h]))) - region: sgp - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="near-intents", region="sgp"}[24h]))) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="near-intents", region="sgp"}) series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="near-intents", region="sgp"}[1h]))) - slug: across @@ -235,12 +235,12 @@ providers: tag: Intent layer formula: "Median wall-clock ms over 24h for Across's Swap API to return a usable USDC quote across supported corridors (Sol to Base, Base to Arb) from 3 regions. Arb to Sol excluded: no destination swaps to Solana." queries: - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="across"}[24h]))) - p90: histogram_quantile(0.90, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="across"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="across"}[24h]))) - mean: sum(rate(bridge_quote_latency_ms_sum{bridge="across"}[24h])) / sum(rate(bridge_quote_latency_ms_count{bridge="across"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="across"}[24h])) - sample_size: sum(increase(bridge_quote_latency_ms_count{bridge="across"}[24h])) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="across"}) + p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="across"}) + p99: histogram_quantile(0.99, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="across"}) + mean: ocb:bridge_quote_latency_ms_sum:rate24h{bridge="across"} / ocb:bridge_quote_latency_ms_count:rate24h{bridge="across"} + success: ocb:bridge_quote_success:avg24h{bridge="across"} + sample_size: ocb:bridge_quote_latency_ms_count:increase24h{bridge="across"} series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="across"}[1h]))) - slug: squid @@ -248,12 +248,12 @@ providers: tag: Aggregator (Axelar) formula: "Median wall-clock ms for Squid Router's v2 API to return a best-output USDC route, p50 over 24h, EVM corridors only (Base to Arb). Solana source excluded: API returns server error." queries: - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="squid"}[24h]))) - p90: histogram_quantile(0.90, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="squid"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="squid"}[24h]))) - mean: sum(rate(bridge_quote_latency_ms_sum{bridge="squid"}[24h])) / sum(rate(bridge_quote_latency_ms_count{bridge="squid"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="squid"}[24h])) - sample_size: sum(increase(bridge_quote_latency_ms_count{bridge="squid"}[24h])) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="squid"}) + p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="squid"}) + p99: histogram_quantile(0.99, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="squid"}) + mean: ocb:bridge_quote_latency_ms_sum:rate24h{bridge="squid"} / ocb:bridge_quote_latency_ms_count:rate24h{bridge="squid"} + success: ocb:bridge_quote_success:avg24h{bridge="squid"} + sample_size: ocb:bridge_quote_latency_ms_count:increase24h{bridge="squid"} series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="squid"}[1h]))) - slug: socket @@ -261,10 +261,10 @@ providers: tag: Aggregator formula: "Median wall-clock ms for Socket Protocol's v2 API to return a best-output USDC route (via Across, Stargate, CCTP), p50 over 24h, EVM corridors only (Base to Arb)." queries: - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="socket"}[24h]))) - p90: histogram_quantile(0.90, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="socket"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="socket"}[24h]))) - mean: sum(rate(bridge_quote_latency_ms_sum{bridge="socket"}[24h])) / sum(rate(bridge_quote_latency_ms_count{bridge="socket"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="socket"}[24h])) - sample_size: sum(increase(bridge_quote_latency_ms_count{bridge="socket"}[24h])) + p50: histogram_quantile(0.50, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="socket"}) + p90: histogram_quantile(0.90, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="socket"}) + p99: histogram_quantile(0.99, ocb:bridge_quote_latency_ms_bucket:rate24h{bridge="socket"}) + mean: ocb:bridge_quote_latency_ms_sum:rate24h{bridge="socket"} / ocb:bridge_quote_latency_ms_count:rate24h{bridge="socket"} + success: ocb:bridge_quote_success:avg24h{bridge="socket"} + sample_size: ocb:bridge_quote_latency_ms_count:increase24h{bridge="socket"} series: histogram_quantile(0.50, sum by (le) (rate(bridge_quote_latency_ms_bucket{bridge="socket"}[1h]))) 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/ethereum-classic-rpc.yml b/benchmarks/ethereum-classic-rpc.yml index 6add2964..c7122e78 100644 --- a/benchmarks/ethereum-classic-rpc.yml +++ b/benchmarks/ethereum-classic-rpc.yml @@ -4,7 +4,7 @@ slug: ethereum-classic-rpc number: "194" title: Fastest free Ethereum Classic RPC, live no-key endpoint latency seo_title: "Fastest free Ethereum Classic RPC 2026" -seo_description: "{{best_name}} leads free Ethereum Classic RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 3 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Ethereum Classic 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 Ethereum Classic EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,13 +14,13 @@ unit: ms higher_is_better: false seo_intro: | - Ethereum Classic (chain 61) is the original Ethereum chain post-2016 DAO fork, continuing as a Proof-of-Work EVM network with ETC as the gas token. The chain upholds code-is-law immutability and has a dedicated long-term holder community. Three keyless endpoints are probed continuously: dRPC (etc.drpc.org), ETCDesktop (etc.etcdesktop.com), and ETCMC (rpc.etcmc.net). + Ethereum Classic (chain 61) is the original Ethereum chain post-2016 DAO fork, continuing as a Proof-of-Work EVM network with ETC as the gas token. The chain upholds code-is-law immutability and has a dedicated long-term holder community. One keyless endpoint is probed continuously: dRPC (etc.drpc.org). ETCDesktop and ETCMC are no longer responding to probes and have been removed. abstract: | Per-chain member of the RPC latency cluster, extended to Ethereum Classic. We measure the round-trip latency of a single, identical RPC call (`eth_getBlockByNumber`) against every no-key public Ethereum Classic endpoint - that sustains continuous probing, 3 providers at launch, + that sustains continuous probing, 1 provider 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 Ethereum Classic-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=\"ethereum-classic\"`. Provider coverage at launch: 3 no-key endpoints." + - "Chain scope: every query on this page is pinned to `chain=\"ethereum-classic\"`. Provider coverage: 1 no-key endpoint (dRPC). ETCDesktop and ETCMC stopped responding to probes and have been removed." findings: - - "{{best_name}} currently leads free Ethereum Classic RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers." + - "{{best_name}} currently leads free Ethereum Classic RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h), 1 measured provider." faq: - q: "What is the fastest free Ethereum Classic 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 Ethereum Classic RPCs work without an API key?" - a: "3 endpoints sustain continuous keyless probing: dRPC, ETCDesktop, and ETCMC. Every endpoint was live-verified before inclusion." + a: "1 endpoint sustains continuous keyless probing: dRPC (etc.drpc.org). ETCDesktop and ETCMC stopped responding to probes and have been removed from the bench." - q: "Does the fastest Ethereum Classic 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 Ethereum Classic RPC latency measured here?" @@ -84,49 +84,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="ethereum-classic", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="ethereum-classic", region="sgp"}[1h]) - - - slug: etcdesktop - name: ETCDesktop - tag: ETCDesktop community public RPC for Ethereum Classic - 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 ETCDesktop's no-key Ethereum Classic endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcdesktop", chain="ethereum-classic"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="etcdesktop", chain="ethereum-classic"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="etcdesktop", chain="ethereum-classic"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="etcdesktop", chain="ethereum-classic"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="etcdesktop", chain="ethereum-classic"}) / sum(ocb:rpc_call:rate_24h{provider="etcdesktop", chain="ethereum-classic"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="etcdesktop", chain="ethereum-classic"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="etcdesktop", chain="ethereum-classic"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcdesktop", chain="ethereum-classic", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="etcdesktop", chain="ethereum-classic", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcdesktop", chain="ethereum-classic", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="etcdesktop", chain="ethereum-classic", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcdesktop", chain="ethereum-classic", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="etcdesktop", chain="ethereum-classic", region="sgp"}[1h]) - - - slug: etcmc - name: ETCMC - tag: ETCMC community public RPC for Ethereum Classic - 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 ETCMC's no-key Ethereum Classic endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcmc", chain="ethereum-classic"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="etcmc", chain="ethereum-classic"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="etcmc", chain="ethereum-classic"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="etcmc", chain="ethereum-classic"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="etcmc", chain="ethereum-classic"}) / sum(ocb:rpc_call:rate_24h{provider="etcmc", chain="ethereum-classic"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="etcmc", chain="ethereum-classic"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="etcmc", chain="ethereum-classic"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcmc", chain="ethereum-classic", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="etcmc", chain="ethereum-classic", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcmc", chain="ethereum-classic", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="etcmc", chain="ethereum-classic", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcmc", chain="ethereum-classic", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="etcmc", chain="ethereum-classic", 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/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/hydration-rpc.yml b/benchmarks/hydration-rpc.yml index 423f4894..61115f2c 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: draft 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,11 +58,14 @@ dimensions: - { value: eu-west, label: EU-West } - { value: sgp, label: Singapore } +# All 5 previously measured providers are DNS dead or returning HTTP 400. +# Suspended 2026-08-07. Will resume when endpoints recover. +# Removed: dwellir, helikon, dotters-network, parachains-network 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." + 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"}) @@ -84,95 +84,3 @@ providers: - 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]) diff --git a/benchmarks/hyperlane-message-latency.yml b/benchmarks/hyperlane-message-latency.yml index 1d15a97d..dbcc2b1a 100644 --- a/benchmarks/hyperlane-message-latency.yml +++ b/benchmarks/hyperlane-message-latency.yml @@ -85,11 +85,11 @@ providers: tag: Ethereum → any chain, Mailbox Dispatch → destination Process delivery formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Ethereum." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="ethereum"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="ethereum"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="ethereum"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="ethereum"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="ethereum"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="ethereum"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="ethereum"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="ethereum"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="ethereum"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="ethereum"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="ethereum"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="ethereum"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="ethereum"}[1h]))) @@ -98,11 +98,11 @@ providers: tag: Polygon PoS → any chain, Bor block + Mailbox delivery formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Polygon." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="polygon"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="polygon"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="polygon"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="polygon"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="polygon"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="polygon"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="polygon"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="polygon"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="polygon"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="polygon"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="polygon"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="polygon"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="polygon"}[1h]))) - slug: arbitrum @@ -110,11 +110,11 @@ providers: tag: Arbitrum One → any chain, sub-second L2 block + Mailbox delivery formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Arbitrum." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="arbitrum"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="arbitrum"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="arbitrum"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="arbitrum"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="arbitrum"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="arbitrum"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="arbitrum"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="arbitrum"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="arbitrum"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="arbitrum"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="arbitrum"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="arbitrum"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="arbitrum"}[1h]))) - slug: base @@ -122,11 +122,11 @@ providers: tag: Base → any chain, 2s block + Mailbox delivery formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Base." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="base"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="base"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="base"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="base"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="base"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="base"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="base"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="base"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="base"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="base"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="base"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="base"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="base"}[1h]))) - slug: sonic @@ -134,163 +134,163 @@ providers: tag: Sonic → any chain, sub-second finality + Mailbox delivery formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Sonic." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="sonic"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="sonic"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="sonic"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="sonic"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="sonic"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="sonic"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="sonic"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="sonic"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="sonic"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="sonic"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="sonic"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="sonic"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="sonic"}[1h]))) - slug: avalanche name: Avalanche tag: Avalanche C-Chain → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Avalanche." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="avalanche"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="avalanche"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="avalanche"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="avalanche"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="avalanche"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="avalanche"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="avalanche"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="avalanche"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="avalanche"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="avalanche"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="avalanche"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="avalanche"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="avalanche"}[1h]))) - slug: berachain name: Berachain tag: Berachain → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Berachain." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="berachain"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="berachain"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="berachain"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="berachain"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="berachain"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="berachain"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="berachain"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="berachain"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="berachain"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="berachain"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="berachain"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="berachain"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="berachain"}[1h]))) - slug: bnb name: BNB Chain tag: BNB Chain → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from BNB Chain." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="bnb"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="bnb"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="bnb"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="bnb"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="bnb"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="bnb"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="bnb"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="bnb"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="bnb"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="bnb"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="bnb"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="bnb"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="bnb"}[1h]))) - slug: celo name: Celo tag: Celo → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Celo." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="celo"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="celo"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="celo"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="celo"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="celo"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="celo"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="celo"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="celo"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="celo"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="celo"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="celo"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="celo"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="celo"}[1h]))) - slug: injective name: Injective tag: Injective (Cosmos) → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Injective." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="injective"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="injective"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="injective"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="injective"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="injective"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="injective"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="injective"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="injective"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="injective"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="injective"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="injective"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="injective"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="injective"}[1h]))) - slug: ink name: Ink tag: Ink L2 → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Ink." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="ink"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="ink"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="ink"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="ink"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="ink"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="ink"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="ink"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="ink"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="ink"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="ink"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="ink"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="ink"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="ink"}[1h]))) - slug: linea name: Linea tag: Linea → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Linea." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="linea"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="linea"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="linea"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="linea"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="linea"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="linea"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="linea"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="linea"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="linea"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="linea"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="linea"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="linea"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="linea"}[1h]))) - slug: mantle name: Mantle tag: Mantle → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Mantle." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="mantle"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="mantle"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="mantle"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="mantle"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="mantle"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="mantle"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="mantle"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="mantle"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="mantle"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="mantle"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="mantle"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="mantle"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="mantle"}[1h]))) - slug: moonbeam name: Moonbeam tag: Moonbeam parachain → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Moonbeam." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="moonbeam"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="moonbeam"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="moonbeam"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="moonbeam"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="moonbeam"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="moonbeam"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="moonbeam"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="moonbeam"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="moonbeam"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="moonbeam"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="moonbeam"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="moonbeam"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="moonbeam"}[1h]))) - slug: optimism name: Optimism tag: Optimism → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Optimism." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="optimism"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="optimism"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="optimism"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="optimism"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="optimism"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="optimism"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="optimism"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="optimism"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="optimism"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="optimism"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="optimism"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="optimism"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="optimism"}[1h]))) - slug: scroll name: Scroll tag: Scroll → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Scroll." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="scroll"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="scroll"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="scroll"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="scroll"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="scroll"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="scroll"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="scroll"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="scroll"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="scroll"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="scroll"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="scroll"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="scroll"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="scroll"}[1h]))) - slug: solana name: Solana tag: Solana → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Solana." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="solana"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="solana"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="solana"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="solana"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="solana"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="solana"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="solana"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="solana"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="solana"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="solana"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="solana"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="solana"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="solana"}[1h]))) - slug: unichain name: Unichain tag: Unichain → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from Unichain." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="unichain"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="unichain"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="unichain"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="unichain"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="unichain"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="unichain"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="unichain"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="unichain"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="unichain"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="unichain"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="unichain"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="unichain"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="unichain"}[1h]))) - slug: world-chain name: World Chain tag: World Chain → any chain formula: "50th percentile over 24h of Hyperlane end-to-end delivery latency for messages sourced from World Chain." queries: - p50: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="world-chain"}[24h]))) - p90: histogram_quantile(0.90, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="world-chain"}[24h]))) - p99: histogram_quantile(0.99, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="world-chain"}[24h]))) - mean: sum(rate(hyperlane_message_latency_milliseconds_sum{source_chain="world-chain"}[24h])) / sum(rate(hyperlane_message_latency_milliseconds_count{source_chain="world-chain"}[24h])) - sample_size: sum(increase(hyperlane_message_seen_total{source_chain="world-chain"}[24h])) + p50: histogram_quantile(0.50, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="world-chain"}) + p90: histogram_quantile(0.90, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="world-chain"}) + p99: histogram_quantile(0.99, ocb:hyperlane_msg_latency_ms_bucket:rate24h{source_chain="world-chain"}) + mean: ocb:hyperlane_msg_latency_ms_sum:rate24h{source_chain="world-chain"} / ocb:hyperlane_msg_latency_ms_count:rate24h{source_chain="world-chain"} + sample_size: ocb:hyperlane_msg_seen:increase24h{source_chain="world-chain"} series: histogram_quantile(0.50, sum by(le) (rate(hyperlane_message_latency_milliseconds_bucket{source_chain="world-chain"}[1h]))) diff --git a/benchmarks/indexing-freshness.yml b/benchmarks/indexing-freshness.yml deleted file mode 100644 index 67cc1f92..00000000 --- a/benchmarks/indexing-freshness.yml +++ /dev/null @@ -1,127 +0,0 @@ -# OpenChainBench. Bench № 070 - -slug: indexing-freshness -number: "070" -title: Freshest wallet data API. Zerion, Allium, Mobula -seo_title: "Freshest wallet data API 2026" -seo_description: "How fast do wallet APIs index a new transaction? Zerion, Allium and Mobula measured live on organic Base transfers, second by second." -subtitle: Seconds between an organic transfer confirming on Base and the moment each wallet data API first returns it, measured continuously on real user transactions. -category: Aggregators -status: live -metric: Indexing freshness -unit: sec -higher_is_better: false - -seo_intro: | - Every wallet app, portfolio tracker and exchange faces the same - question. when a user receives funds, how long before the API my - app is built on actually shows it. Providers advertise "real-time" - and "sub-second indexing"; none publish comparable numbers. This - benchmark measures it the only way that cannot be gamed. we pick a - random, organic native transfer from the newest Base block, real - user, different wallet every time, and poll each provider's wallet - transactions API until the tx hash appears. Because the ground - truth is a random real transaction there is no benchmark wallet a - provider could special-case, and every measurement is publicly - re-verifiable from the tx hash. Cohort. Zerion, Allium, - GoldRush (Covalent) and Mobula, all probed with the identical - schedule from the same host. The timing context matters in 2026. - Dune Sim is sunsetting and SimpleHash is gone, so teams are - choosing a replacement wallet data API right now, and freshness is - the spec sheet line that separates them. - -abstract: | - We measure the visibility lag of wallet data APIs. the time between - an organic native transfer confirming on Base (T0 = the instant our - own RPC observes the containing block) and the first poll at which - each provider's wallet transactions endpoint returns that tx hash. - One probe event per 10 minutes; each event uses a fresh random - transaction from a fresh block, so wallets are cold and results - include any lazy, on-demand indexing path a provider runs for - never-before-queried addresses. Detection is parser-free (tx hash - substring in the raw response), the poll schedule is front-loaded - (1s to 120s, identical for every provider), and per-provider - monthly quota guards keep the probe inside every free tier. An - event a provider has not indexed within 120 seconds counts as - missed, which feeds the reliability column, because an API that - never shows the deposit is worse than a slow one. - -methodology: - - "Ground truth: one probe event per 10 minutes. The harness watches new Base blocks through its own RPC and picks one random plain native transfer (value > 0, empty calldata, OP-stack system deposit excluded). T0 is the instant the harness observes the containing block; the same T0 is used for every provider." - - "Anti-gaming by construction: the measured wallet is a random real user's address, different on every event, so no provider can whitelist or pre-warm a known benchmark wallet. Every measurement is re-verifiable by anyone from the public tx hash." - - "Poll schedule: each provider's wallet transactions endpoint is polled at 1, 2, 3, 4, 6, 8, 11, 15, 20, 26, 34, 45, 60, 80, 100 and 120 seconds after T0. Reported lag is the first poll at which the response contains the tx hash, an upper bound with resolution equal to the gap between consecutive polls." - - "Detection is parser-free: the raw response body is scanned for the tx hash substring. Every cohort API returns the hash verbatim, so no provider gains or loses from response schema differences." - - "Cold wallets by design: because each event uses a never-before-queried address, results include any on-demand indexing path a provider runs for new wallets. This mirrors the experience of a user opening an app on a fresh address, and it is disclosed here because warm, continuously-queried wallets may see lower lags." - - "Classification: found (lag recorded), missed (not indexed within 120s), api_error (provider errored on every poll of the event). Percentiles are computed from the histogram of found lags over 24h; the miss rate is published alongside because latency without reliability is a misleading ranking signal." - - "Quota fairness: per-provider monthly call budgets sized to each free tier with headroom, enforced by a guard that pauses probing at 90%. Allium participates in every third event (20k calls/month free tier); all other providers join every event. Sample sizes per provider are published." - - "Scope: Base mainnet, single probe region. Freshness lags are measured in seconds while cross-region network deltas are milliseconds, so multi-region probing would add cost without changing the ranking; this is revisited if two providers converge within one poll-step of each other." - -findings: - - "{{best_name}} currently leads at {{best_p50}} (p50 of found lags, 24h) across organic Base transfers." - - "{{name:zerion}} sits at {{p50:zerion}}. Early runs showed a bimodal pattern, some events indexed in about a second and others surfacing several seconds later, consistent with a caching layer in front of the wallet endpoint." - - "{{name:goldrush}} trails at {{p50:goldrush}} on this probe. Its unified multi-chain schema trades freshness for breadth, a real trade-off teams should weigh explicitly." - - "Miss rates matter more than medians: an API that fails to show a deposit within two minutes breaks the user flow entirely, so read the reliability column before the latency one." - -faq: - - q: "Which wallet data API shows new transactions fastest?" - a: "Per the live leaderboard above: {{best_name}} at {{best_p50}} (p50 over 24h of organic Base transfers). The ranking re-sorts continuously as probe events land every 10 minutes. Check the miss-rate column too, a provider that occasionally never indexes a transfer is worse for a wallet app than one that is a second slower on median." - - q: "How is indexing freshness measured here?" - a: "The harness picks a random organic native transfer from the newest Base block, records T0 when its own RPC observes the block, then polls every provider's wallet transactions endpoint on an identical front-loaded schedule (1s to 120s) until the tx hash appears in the raw response. The lag is the first successful poll. Events a provider has not indexed within 120 seconds count as missed. The full harness is open source and each measurement can be re-verified from the public tx hash." - - q: "Why use random real transactions instead of a controlled test wallet?" - a: "Two reasons. First, integrity: a fixed benchmark wallet could be whitelisted or pre-warmed by a provider; a random real user's transfer, different every event, cannot. Second, realism: cold, never-before-queried addresses exercise any lazy indexing path a provider runs for new wallets, which is exactly what a new user experiences. The trade-off, disclosed in the methodology, is that continuously-queried warm wallets may see lower lags than reported here." - - q: "Does this benchmark cover more chains than Base?" - a: "Not yet. Base was chosen first for its high volume of plain native transfers (dense organic ground truth) and 2-second blocks. The harness is chain-agnostic and additional EVM chains join by adding an RPC endpoint, subject to each provider's free-tier quota budget. Chain coverage itself differs per provider and is part of what teams should evaluate." - - q: "Why does freshness matter when choosing a wallet API in 2026?" - a: "Because the market is consolidating. Dune Sim is sunsetting in August 2026 and SimpleHash shut down in 2025, so many teams are migrating to a new wallet data API right now. Providers advertise real-time indexing but publish no comparable numbers; deposit visibility lag is the difference between a user seeing their funds arrive and a support ticket. This page is the only continuously measured, provider-neutral comparison of that number." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/indexing-freshness - -prometheus: - window: 24h - freshness_metric: indexing_freshness_seconds - -providers: - - slug: mobula - name: Mobula - tag: Wallet + market data API, 50+ chains - formula: "Median (p50) over 24h of visibility lag in seconds between an organic Base transfer confirming and Mobula's wallet transactions endpoint first returning it, from the shared histogram estimator." - queries: - p50: histogram_quantile(0.5, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="mobula"}[24h]))) - p90: histogram_quantile(0.9, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="mobula"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="mobula"}[24h]))) - success: sum(increase(indexing_probe_total{provider="mobula", result="found"}[24h])) / sum(increase(indexing_probe_total{provider="mobula", result=~"found|missed"}[24h])) - sample_size: sum(increase(indexing_probe_total{provider="mobula", result=~"found|missed"}[24h])) - series: avg_over_time(indexing_freshness_seconds{provider="mobula"}[1h]) - - slug: zerion - name: Zerion - tag: Wallet API behind the Zerion app, 25+ chains - formula: "Median (p50) over 24h of visibility lag in seconds between an organic Base transfer confirming and Zerion's wallet transactions endpoint first returning it, from the shared histogram estimator." - queries: - p50: histogram_quantile(0.5, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="zerion"}[24h]))) - p90: histogram_quantile(0.9, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="zerion"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="zerion"}[24h]))) - success: sum(increase(indexing_probe_total{provider="zerion", result="found"}[24h])) / sum(increase(indexing_probe_total{provider="zerion", result=~"found|missed"}[24h])) - sample_size: sum(increase(indexing_probe_total{provider="zerion", result=~"found|missed"}[24h])) - series: avg_over_time(indexing_freshness_seconds{provider="zerion"}[1h]) - - slug: goldrush - name: GoldRush - tag: Covalent's multi-chain wallet API, unified schema - formula: "Median (p50) over 24h of visibility lag in seconds between an organic Base transfer confirming and GoldRush's transactions endpoint first returning it, from the shared histogram estimator." - queries: - p50: histogram_quantile(0.5, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="goldrush"}[24h]))) - p90: histogram_quantile(0.9, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="goldrush"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="goldrush"}[24h]))) - success: sum(increase(indexing_probe_total{provider="goldrush", result="found"}[24h])) / sum(increase(indexing_probe_total{provider="goldrush", result=~"found|missed"}[24h])) - sample_size: sum(increase(indexing_probe_total{provider="goldrush", result=~"found|missed"}[24h])) - series: avg_over_time(indexing_freshness_seconds{provider="goldrush"}[1h]) - - slug: allium - name: Allium - tag: Enterprise realtime wallet APIs, 100+ chains - formula: "Median (p50) over 24h of visibility lag in seconds between an organic Base transfer confirming and Allium's wallet transactions endpoint first returning it. Allium joins every third event to respect its free-tier quota." - queries: - p50: histogram_quantile(0.5, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="allium"}[24h]))) - p90: histogram_quantile(0.9, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="allium"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="allium"}[24h]))) - success: sum(increase(indexing_probe_total{provider="allium", result="found"}[24h])) / sum(increase(indexing_probe_total{provider="allium", result=~"found|missed"}[24h])) - sample_size: sum(increase(indexing_probe_total{provider="allium", result=~"found|missed"}[24h])) - series: avg_over_time(indexing_freshness_seconds{provider="allium"}[1h]) diff --git a/benchmarks/kaia-rpc.yml b/benchmarks/kaia-rpc.yml index 24a1c0b3..38c54c32 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/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/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/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/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 db727d94..8510877d 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,71 +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: 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 - 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 53136be7..09ed0d0f 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 @@ -107,26 +84,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/token-quote-coverage.yml b/benchmarks/token-quote-coverage.yml index f0c2874c..73f2430b 100644 --- a/benchmarks/token-quote-coverage.yml +++ b/benchmarks/token-quote-coverage.yml @@ -77,7 +77,7 @@ methodology: - "Aggregator endpoints: Jupiter lite-api.jup.ag/swap/v1/quote (Solana), Mobula /api/2/swap/quoting (EVM + Solana, API key), Relay api.relay.link/quote (EVM + Solana, no key), KyberSwap aggregator-api.kyberswap.com/{chain}/api/v1/routes (EVM only, no key), ParaSwap apiv5.paraswap.io/prices (EVM only, no key). Each adapter normalises chain identifiers, input/output decimals, and error response shapes before recording outcome." - "Jupiter strictMode=false: queried without the curated-token filter so Jupiter will attempt routing for any token with a discoverable on-chain pool, matching how most wallets call the API. strictMode=true would trivially exclude new tokens and defeat the purpose of the benchmark." - "Chain-scope enforcement: Jupiter probes Solana-only. KyberSwap and ParaSwap probe EVM only (Base, BNB, Robinhood). Mobula and Relay probe all four chains including Solana. Providers are never penalised for chains they do not support: null cells are excluded from cross-chain aggregates." - - "Coverage metric: three Prometheus counters per probe. token_quote_coverage_attempts_total (every probe), token_quote_api_ok_total (HTTP 200 received, regardless of outAmount), and token_quote_coverage_success_total (HTTP 200 + outAmount > 0). The headline coverage rate (p50 slot) remains success / attempts for historical continuity. Two derived slots expose the split: availability = api_ok / attempts (is the API reachable and returning 200?); coverage_given_available = success / api_ok (given the API responded, did it find a route?)." + - "Coverage metric: three counters per probe: attempts_total (every probe), api_ok_total (HTTP 200 received), success_total (HTTP 200 + outAmount > 0). Headline rate = success / attempts. Two derived slots: availability = api_ok / attempts; coverage_given_available = success / api_ok." - "Probe cadence and deduplication: each batch cycles through the current Dexscreener token-boosts feed. Tokens are not deduplicated across batches; the same token may appear in multiple 30-minute windows if it remains boosted. The score therefore reflects freshness of the aggregator's current routing graph, not historical coverage depth." - "Single origin: all probes originate from the eu-west OCB VPS. Regional latency is not a factor in this bench (the outcome is binary hit/miss, not latency). No region dimension in v1." diff --git a/benchmarks/tron-rpc.yml b/benchmarks/tron-rpc.yml index 627b2521..1657e029 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), Ankr (key required on tron_jsonrpc path), Chainstack / NOWNodes / GetBlock / Tatum / BlockPI (key required on JSON-RPC path), OnFinality + Blast API + AllThatNode (no public TRON JSON-RPC gateway), Tron Foundation's rpc.trongrid.io (DNS dead; correct 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 0bb99648..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?" @@ -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/xdc-rpc.yml b/benchmarks/xdc-rpc.yml index e4c64a92..493e78f5 100644 --- a/benchmarks/xdc-rpc.yml +++ b/benchmarks/xdc-rpc.yml @@ -4,7 +4,7 @@ slug: xdc-rpc number: "153" title: Fastest free XDC Network RPC, live no-key endpoint latency seo_title: "Fastest free XDC Network RPC 2026" -seo_description: "{{best_name}} leads free XDC Network RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 5 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free XDC 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 XDC Network RPC endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,13 +14,13 @@ unit: ms higher_is_better: false seo_intro: | - XDC Network (chain 50) is an EVM-compatible hybrid blockchain targeting enterprise and trade-finance use cases, XDC gas token, delegated proof of stake consensus, designed for ISO 20022-compliant financial applications. The XDC Foundation's rpc.xinfin.network and additional endpoints from Ankr and the community provide keyless access probed every 60 seconds from three regions. + XDC Network (chain 50) is an EVM-compatible hybrid blockchain targeting enterprise and trade-finance use cases, XDC gas token, delegated proof of stake consensus, designed for ISO 20022-compliant financial applications. Two keyless endpoints are probed continuously: Ankr and XDC eRPC (erpc.xinfin.network). XDC Foundation official, XDCrpc, and XDC.org stopped responding to probes and have been removed. abstract: | Per-chain member of the RPC latency cluster, extended to XDC Network. We measure the round-trip latency of a single, identical RPC call (`eth_getBlockByNumber`) against every no-key public XDC Network endpoint - that sustains continuous probing, 5 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=\"xdc\"`. Provider coverage at launch: 5 no-key endpoints (XDC Foundation, Ankr, XDCrpc, XDC eRPC, XDC.org)." + - "Chain scope: every query on this page is pinned to `chain=\"xdc\"`. Provider coverage: 2 no-key endpoints (Ankr, XDC eRPC). XDC Foundation official, XDCrpc and XDC.org stopped responding to probes and have been removed." findings: - - "{{best_name}} currently leads free XDC Network RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 5 measured providers." + - "{{best_name}} currently leads free XDC Network RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 2 measured providers." faq: - q: "What is the fastest free XDC Network 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. 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 XDC Network RPCs work without an API key?" - a: "5 endpoints sustain continuous keyless probing at launch: XDC Foundation, Ankr, XDCrpc, XDC eRPC, XDC.org. Every listed endpoint was live-verified before inclusion." + a: "2 endpoints sustain continuous keyless probing: Ankr and XDC eRPC (erpc.xinfin.network). XDC Foundation official, XDCrpc and XDC.org stopped responding to probes and have been removed." - q: "Does the fastest XDC 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 XDC Network RPC latency measured here?" @@ -65,29 +65,6 @@ dimensions: - { value: sgp, label: Singapore } providers: - - slug: xdc-official - name: XDC Foundation - tag: XDC Foundation's primary public 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 XDC Foundation's no-key XDC Network endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-official", chain="xdc"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="xdc-official", chain="xdc"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="xdc-official", chain="xdc"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="xdc-official", chain="xdc"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="xdc-official", chain="xdc"}) / sum(ocb:rpc_call:rate_24h{provider="xdc-official", chain="xdc"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="xdc-official", chain="xdc"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="xdc-official", chain="xdc"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-official", chain="xdc", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdc-official", chain="xdc", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-official", chain="xdc", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdc-official", chain="xdc", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-official", chain="xdc", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdc-official", chain="xdc", region="sgp"}[1h]) - - slug: ankr name: Ankr tag: Multi-cloud RPC network @@ -111,29 +88,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="xdc", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="xdc", region="sgp"}[1h]) - - slug: xdcrpc - name: XDCrpc - tag: Community-operated XDC public 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 XDCrpc's no-key XDC Network endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdcrpc", chain="xdc"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="xdcrpc", chain="xdc"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="xdcrpc", chain="xdc"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="xdcrpc", chain="xdc"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="xdcrpc", chain="xdc"}) / sum(ocb:rpc_call:rate_24h{provider="xdcrpc", chain="xdc"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="xdcrpc", chain="xdc"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="xdcrpc", chain="xdc"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdcrpc", chain="xdc", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdcrpc", chain="xdc", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdcrpc", chain="xdc", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdcrpc", chain="xdc", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdcrpc", chain="xdc", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdcrpc", chain="xdc", region="sgp"}[1h]) - - slug: xdc-erpc name: XDC eRPC tag: XDC Foundation alternate public RPC gateway @@ -156,26 +110,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-erpc", chain="xdc", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="xdc-erpc", chain="xdc", region="sgp"}[1h]) - - - slug: xdc-org - name: XDC.org - tag: XDC Foundation third public RPC mirror - 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 XDC Foundation's rpc.xdc.org endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-org", chain="xdc"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="xdc-org", chain="xdc"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="xdc-org", chain="xdc"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="xdc-org", chain="xdc"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="xdc-org", chain="xdc"}) / sum(ocb:rpc_call:rate_24h{provider="xdc-org", chain="xdc"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="xdc-org", chain="xdc"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="xdc-org", chain="xdc"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-org", chain="xdc", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdc-org", chain="xdc", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-org", chain="xdc", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdc-org", chain="xdc", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-org", chain="xdc", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdc-org", chain="xdc", region="sgp"}[1h]) diff --git a/benchmarks/zetachain-rpc.yml b/benchmarks/zetachain-rpc.yml index 2b9a1bba..9054761d 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,48 +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]) - - - 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/aggregator-head-lag/Dockerfile b/harnesses/aggregator-head-lag/Dockerfile index 74ca8f0f..777b00d3 100644 --- a/harnesses/aggregator-head-lag/Dockerfile +++ b/harnesses/aggregator-head-lag/Dockerfile @@ -21,9 +21,12 @@ FROM debian:bookworm-slim WORKDIR /app -# Install runtime dependencies +# Install runtime dependencies + Chromium for in-process JWE scraping RUN apt-get update && apt-get install -y \ ca-certificates \ + chromium \ + chromium-sandbox \ + fonts-liberation \ && rm -rf /var/lib/apt/lists/* # Copy binary from builder diff --git a/harnesses/aggregator-head-lag/cmd/scrape-cookie/main.go b/harnesses/aggregator-head-lag/cmd/scrape-cookie/main.go new file mode 100644 index 00000000..f64c3df5 --- /dev/null +++ b/harnesses/aggregator-head-lag/cmd/scrape-cookie/main.go @@ -0,0 +1,150 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "os" + "time" + + "github.com/chromedp/cdproto/network" + "github.com/chromedp/chromedp" +) + +func main() { + opts := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.Flag("headless", true), + chromedp.Flag("disable-gpu", true), + chromedp.Flag("no-sandbox", true), + chromedp.Flag("disable-dev-shm-usage", true), + chromedp.UserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"), + ) + + allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) + defer cancel() + ctx, cancel := chromedp.NewContext(allocCtx) + defer cancel() + ctx, cancel = context.WithTimeout(ctx, 60*time.Second) + defer cancel() + + cookies := map[string]string{} + + // Monkey-patch WebSocket to capture connection_init sent to graph.codex.io + // and the server's first response (ack or error/close). + captureScript := ` +window.__wsCaptures = []; +window.__wsResponses = []; +const _WS = window.WebSocket; +window.WebSocket = function(url, protocols) { + const ws = new _WS(url, protocols); + if (url && url.includes('graph.codex.io')) { + const _send = ws.send.bind(ws); + ws.send = function(data) { + try { + const parsed = JSON.parse(data); + if (parsed.type === 'connection_init') { + window.__wsCaptures.push({url: url, payload: parsed.payload, raw: data}); + console.log('WS_CAPTURE:' + JSON.stringify({url, payload: parsed.payload})); + } + } catch(e) {} + return _send(data); + }; + ws.addEventListener('message', function(evt) { + try { + const parsed = JSON.parse(evt.data); + window.__wsResponses.push({url: url, type: parsed.type, raw: evt.data.slice(0, 200)}); + console.log('WS_RESPONSE:' + JSON.stringify({url, type: parsed.type})); + } catch(e) {} + }); + ws.addEventListener('close', function(evt) { + window.__wsResponses.push({url: url, closeCode: evt.code, reason: evt.reason}); + console.log('WS_CLOSE:' + JSON.stringify({url, code: evt.code, reason: evt.reason})); + }); + } + return ws; +}; +window.WebSocket.prototype = _WS.prototype; +window.WebSocket.CONNECTING = _WS.CONNECTING; +window.WebSocket.OPEN = _WS.OPEN; +window.WebSocket.CLOSING = _WS.CLOSING; +window.WebSocket.CLOSED = _WS.CLOSED; +` + + err := chromedp.Run(ctx, + // Inject monkey-patch before page loads + chromedp.ActionFunc(func(ctx context.Context) error { + return chromedp.Evaluate(`void 0`, nil).Do(ctx) // warm up + }), + chromedp.Navigate("about:blank"), + chromedp.Evaluate(captureScript, nil), + chromedp.Navigate("https://www.defined.fi/"), + chromedp.WaitVisible(`body`, chromedp.ByQuery), + // Re-inject on the loaded page (navigate clears scripts) + chromedp.Evaluate(captureScript, nil), + chromedp.Sleep(20*time.Second), + chromedp.ActionFunc(func(ctx context.Context) error { + // Get cookies + cookieParams, err := network.GetCookies().Do(ctx) + if err != nil { + return fmt.Errorf("failed to get cookies: %w", err) + } + for _, cookie := range cookieParams { + cookies[cookie.Name] = cookie.Value + fmt.Fprintf(os.Stderr, "cookie: %s (len=%d)\n", cookie.Name, len(cookie.Value)) + } + return nil + }), + // Capture what the browser sent in connection_init + chromedp.ActionFunc(func(ctx context.Context) error { + var captures []map[string]interface{} + err := chromedp.Evaluate(`window.__wsCaptures || []`, &captures).Do(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "capture eval error: %v\n", err) + return nil + } + if len(captures) == 0 { + fmt.Fprintf(os.Stderr, "\n[WS CAPTURE] No connection_init captured for graph.codex.io\n") + // Try to get captures via console approach + var raw string + chromedp.Evaluate(`JSON.stringify(window.__wsCaptures || [])`, &raw).Do(ctx) + fmt.Fprintf(os.Stderr, "[WS CAPTURE] raw: %s\n", raw) + } + for i, c := range captures { + fmt.Fprintf(os.Stderr, "\n[WS CAPTURE #%d] url=%v\n", i, c["url"]) + payloadBytes, _ := json.MarshalIndent(c["payload"], "", " ") + fmt.Fprintf(os.Stderr, "[WS CAPTURE #%d] connection_init payload:\n%s\n", i, string(payloadBytes)) + fmt.Printf("WS_INIT_PAYLOAD=%s\n", string(payloadBytes)) + } + // Capture server responses (ack / close) + var responses []map[string]interface{} + chromedp.Evaluate(`window.__wsResponses || []`, &responses).Do(ctx) + if len(responses) == 0 { + fmt.Fprintf(os.Stderr, "\n[WS RESPONSE] No server responses captured yet\n") + } + for i, r := range responses { + respBytes, _ := json.MarshalIndent(r, "", " ") + fmt.Fprintf(os.Stderr, "\n[WS RESPONSE #%d]:\n%s\n", i, string(respBytes)) + } + return nil + }), + ) + if err != nil { + fmt.Fprintf(os.Stderr, "chrome error: %v\n", err) + os.Exit(1) + } + + // Output cookies + if attToken, ok := cookies["defined-attestation-token"]; ok { + fmt.Printf("DEFINED_SESSION_COOKIE=%s\n", attToken) + } + if raw, ok := cookies["codex_token"]; ok { + decoded, _ := url.QueryUnescape(raw) + var obj struct { + Token string `json:"token"` + } + if json.Unmarshal([]byte(decoded), &obj) == nil { + fmt.Printf("CODEX_TOKEN=%s\n", obj.Token) + } + } +} diff --git a/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go b/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go new file mode 100644 index 00000000..41740bd4 --- /dev/null +++ b/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go @@ -0,0 +1,162 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "os" + "strings" + "sync" + "time" + + "github.com/chromedp/cdproto/network" + "github.com/chromedp/chromedp" +) + +// in-process JWE refresher: scrapes defined.fi every 5 min via headless Chrome. +// Result is stored here so GetDefinedJWTToken can use it without hitting the Paris box. +var inProcessJWE struct { + sync.RWMutex + token string + mintedAt time.Time +} + +func getInProcessJWE() (string, time.Time) { + inProcessJWE.RLock() + defer inProcessJWE.RUnlock() + return inProcessJWE.token, inProcessJWE.mintedAt +} + +func setInProcessJWE(token string) { + inProcessJWE.Lock() + defer inProcessJWE.Unlock() + inProcessJWE.token = token + inProcessJWE.mintedAt = time.Now() +} + +// scrapeCodexToken runs headless Chrome, navigates to defined.fi, and extracts the codex_token JWE. +func scrapeCodexToken() (string, error) { + opts := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.Flag("headless", true), + chromedp.Flag("disable-gpu", true), + chromedp.Flag("no-sandbox", true), + chromedp.Flag("disable-dev-shm-usage", true), + chromedp.UserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"), + ) + + // Allow overriding Chrome binary path via env (useful for Alpine/Debian containers). + if chromePath := os.Getenv("CHROME_PATH"); chromePath == "" { + // Try common locations + for _, p := range []string{"/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome"} { + if _, err := os.Stat(p); err == nil { + opts = append(opts, chromedp.ExecPath(p)) + break + } + } + } else { + opts = append(opts, chromedp.ExecPath(chromePath)) + } + + allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), opts...) + defer allocCancel() + + ctx, cancel := chromedp.NewContext(allocCtx) + defer cancel() + + ctx, cancel = context.WithTimeout(ctx, 90*time.Second) + defer cancel() + + var cookies []*network.Cookie + err := chromedp.Run(ctx, + chromedp.Navigate("https://www.defined.fi/"), + chromedp.WaitVisible(`body`, chromedp.ByQuery), + chromedp.Sleep(20*time.Second), + chromedp.ActionFunc(func(ctx context.Context) error { + var err error + cookies, err = network.GetCookies().Do(ctx) + return err + }), + ) + if err != nil { + return "", fmt.Errorf("chromedp run failed: %w", err) + } + + for _, c := range cookies { + if c.Name == "codex_token" { + decoded, err := url.QueryUnescape(c.Value) + if err != nil { + return "", fmt.Errorf("url unescape failed: %w", err) + } + var obj struct { + Token string `json:"token"` + } + if err := json.Unmarshal([]byte(decoded), &obj); err == nil && obj.Token != "" { + return obj.Token, nil + } + // Fallback: if the cookie value is the JWE directly + if strings.HasPrefix(decoded, "eyJ") { + return decoded, nil + } + } + } + return "", fmt.Errorf("codex_token cookie not found after page load") +} + +// chromeAvailable returns true if a Chrome/Chromium binary is found on this host. +func chromeAvailable() bool { + for _, p := range []string{ + os.Getenv("CHROME_PATH"), + "/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable", + } { + if p != "" { + if _, err := os.Stat(p); err == nil { + return true + } + } + } + return false +} + +// startInProcessScraper launches a background goroutine that refreshes the JWE every 5 min. +// Call once from main. No-ops silently if Chrome is not installed. +func startInProcessScraper(stopChan <-chan struct{}) { + if !chromeAvailable() { + fmt.Println("[CODEX-SCRAPER] Chrome not found — in-process scraper disabled (sidecar will be used)") + return + } + + go func() { + // Initial delay: let the container fully start before launching Chrome. + select { + case <-stopChan: + return + case <-time.After(10 * time.Second): + } + + refreshInterval := 5 * time.Minute + + for { + fmt.Println("[CODEX-SCRAPER] Scraping defined.fi for fresh JWE...") + tok, err := scrapeCodexToken() + if err != nil { + fmt.Printf("[CODEX-SCRAPER] Scrape failed: %v — retrying in 60s\n", err) + select { + case <-stopChan: + return + case <-time.After(60 * time.Second): + continue + } + } + setInProcessJWE(tok) + fmt.Printf("[CODEX-SCRAPER] Fresh JWE stored (len=%d), next refresh in %v\n", len(tok), refreshInterval) + + select { + case <-stopChan: + return + case <-time.After(refreshInterval): + } + } + }() +} diff --git a/harnesses/aggregator-head-lag/cmd/script/defined_auth.go b/harnesses/aggregator-head-lag/cmd/script/defined_auth.go index 716f05f0..c38dfd56 100644 --- a/harnesses/aggregator-head-lag/cmd/script/defined_auth.go +++ b/harnesses/aggregator-head-lag/cmd/script/defined_auth.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "os" "strings" "sync" "time" @@ -57,8 +58,110 @@ func decodeJWTExpiration(token string) (time.Time, error) { return time.Unix(claims.Exp, 0), nil } -// GetDefinedJWTToken returns a cached JWT token or generates a new one if expired +// tryDirectCodexToken calls /api/codex/token on defined.fi directly using the session cookie. +// No proxy so the JWE is minted from this container's own IP, matching the WS connection IP. +func tryDirectCodexToken(sessionCookie string) (string, error) { + if sessionCookie == "" { + return "", fmt.Errorf("no session cookie") + } + client := &http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{ + DisableKeepAlives: true, + }, + } + req, err := http.NewRequest("GET", "https://www.defined.fi/api/codex/token", nil) + if err != nil { + return "", err + } + req.Header.Set("Accept", "application/json") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req.Header.Set("Origin", "https://www.defined.fi") + req.Header.Set("Referer", "https://www.defined.fi/") + req.Header.Set("sec-ch-ua", `"Not_A Brand";v="8", "Chromium";v="131", "Google Chrome";v="131"`) + req.Header.Set("sec-ch-ua-mobile", "?0") + req.Header.Set("sec-ch-ua-platform", `"macOS"`) + req.Header.Set("sec-fetch-dest", "empty") + req.Header.Set("sec-fetch-mode", "cors") + req.Header.Set("sec-fetch-site", "same-origin") + req.AddCookie(&http.Cookie{Name: "defined-attestation-token", Value: sessionCookie}) + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != 200 { + return "", fmt.Errorf("status %d: %.100s", resp.StatusCode, string(body)) + } + var parsed struct { + Token string `json:"token"` + } + if err := json.Unmarshal(body, &parsed); err == nil && parsed.Token != "" { + return parsed.Token, nil + } + return "", fmt.Errorf("no token in response: %.100s", string(body)) +} + +// tryTokenService calls the Paris-box sidecar (DEFINED_TOKEN_SERVICE_URL) for a fresh JWE. +func tryTokenService(baseURL string) (string, error) { + client := &http.Client{Timeout: 5 * time.Second} + for _, path := range []string{"/token", "/jwt", "/"} { + resp, err := client.Get(baseURL + path) + if err != nil { + continue + } + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + if resp.StatusCode != 200 { + continue + } + var parsed struct{ Token string `json:"token"` } + if err := json.Unmarshal(body, &parsed); err == nil && parsed.Token != "" { + return parsed.Token, nil + } + s := strings.TrimSpace(string(body)) + if len(s) > 50 { + return s, nil + } + } + return "", fmt.Errorf("no working path on sidecar") +} + +// GetDefinedJWTToken returns a cached JWT token or generates a new one if expired. +// Priority: CODEX_JWT env var > in-process scraper (fresh, <5min) > direct /api/codex/token > sidecar > inline mint. func GetDefinedJWTToken(sessionCookie string) (string, error) { + if jwt := os.Getenv("CODEX_JWT"); jwt != "" { + return jwt, nil + } + // In-process scraper: headless Chrome running inside this container, freshest possible. + // JWE expires in ~10 min; scraper refreshes every 5 min so token is always <5 min old. + if tok, mintedAt := getInProcessJWE(); tok != "" && time.Since(mintedAt) < 8*time.Minute { + fmt.Printf("[DEFINED-AUTH] Got token from in-process scraper (age=%v, len=%d)\n", time.Since(mintedAt).Round(time.Second), len(tok)) + return tok, nil + } + // utls Chrome fingerprint scraper: visits defined.fi, gets CSRF, POSTs to /api/codex/token. + // Works if this container's IP is not in Vercel's datacenter blocklist. + if tok, err := tryUtlsCodexToken(); err == nil && tok != "" { + fmt.Printf("[DEFINED-AUTH] Got token via utls scraper (len=%d)\n", len(tok)) + return tok, nil + } else { + fmt.Printf("[DEFINED-AUTH] utls scraper failed: %v\n", err) + } + // Direct mint (standard Go TLS, usually blocked by Vercel bot check). + if tok, err := tryDirectCodexToken(sessionCookie); err == nil && tok != "" { + fmt.Printf("[DEFINED-AUTH] Got token via direct /api/codex/token (len=%d)\n", len(tok)) + return tok, nil + } + // Sidecar fallback (Paris box, Mac-push keeps it fresh every 5 min) + if svcURL := os.Getenv("DEFINED_TOKEN_SERVICE_URL"); svcURL != "" { + if tok, err := tryTokenService(svcURL); err == nil && tok != "" { + fmt.Printf("[DEFINED-AUTH] Got token from sidecar (len=%d)\n", len(tok)) + return tok, nil + } + } + globalTokenCache.mu.RLock() // Check if we have a valid cached token @@ -104,49 +207,39 @@ func GetDefinedJWTToken(sessionCookie string) (string, error) { return token, nil } -// generateDefinedJWTToken generates a new JWT token from Defined.fi session cookie -func generateDefinedJWTToken(sessionCookie string) (string, error) { - fmt.Println("[DEFINED-AUTH] Generating new JWT token from Defined.fi (local)...") - fmt.Println("[DEFINED-AUTH] Creating new HTTP client with fresh TCP connection (no keepalive)") - - // Create a new HTTP client with fresh connection for each request. - // CRITICAL: route through HTTP_PROXY/HTTPS_PROXY (webshare rotating proxy) - // so each JWT mint hits a fresh IP. Direct from the container IP gets us - // stuck on Vercel's bot ban (429 loop) when the container restarts often. - transport := &http.Transport{ - DisableKeepAlives: true, - MaxIdleConnsPerHost: 0, - Proxy: http.ProxyFromEnvironment, - } - client := &http.Client{ - Timeout: 10 * time.Second, - Transport: transport, - } +// generateDefinedJWTToken generates a new JWT token via un.defined.fi (old UI, still alive). +// No session cookie or CSRF needed. Falls back to www.defined.fi/api proxy path. +func generateDefinedJWTToken(_ string) (string, error) { + fmt.Println("[DEFINED-AUTH] Generating token via un.defined.fi legacy API...") reqBody := map[string]interface{}{ "operationName": "CreateApiToken", "query": "mutation CreateApiToken { createApiTokens(input: { count: 1 }) { token } }", "variables": map[string]interface{}{}, } - bodyBytes, _ := json.Marshal(reqBody) - req, _ := http.NewRequest("POST", "https://www.defined.fi/api", bytes.NewBuffer(bodyBytes)) + transport := &http.Transport{ + DisableKeepAlives: true, + Proxy: http.ProxyFromEnvironment, + } + client := &http.Client{Timeout: 10 * time.Second, Transport: transport} + + req, _ := http.NewRequest("POST", "https://un.defined.fi/api", bytes.NewBuffer(bodyBytes)) req.Header.Set("Accept", "application/json") - req.Header.Set("Accept-Language", "en-US,en;q=0.9") req.Header.Set("Content-Type", "application/json") - req.Header.Set("Origin", "https://www.defined.fi") - req.Header.Set("Referer", "https://www.defined.fi/") req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + req.Header.Set("Origin", "https://un.defined.fi") + req.Header.Set("Referer", "https://un.defined.fi/") req.Header.Set("sec-ch-ua", `"Not_A Brand";v="8", "Chromium";v="131", "Google Chrome";v="131"`) req.Header.Set("sec-ch-ua-mobile", "?0") req.Header.Set("sec-ch-ua-platform", `"macOS"`) req.Header.Set("sec-fetch-dest", "empty") req.Header.Set("sec-fetch-mode", "cors") req.Header.Set("sec-fetch-site", "same-origin") - req.AddCookie(&http.Cookie{Name: "session", Value: sessionCookie}) - fmt.Println("[DEFINED-AUTH] Sending POST request to https://www.defined.fi/api...") + fmt.Println("[DEFINED-AUTH] POST https://un.defined.fi/api ...") resp, err := client.Do(req) if err != nil { fmt.Printf("[DEFINED-AUTH] ❌ Request failed: %v\n", err) @@ -158,18 +251,16 @@ func generateDefinedJWTToken(sessionCookie string) (string, error) { fmt.Printf("[DEFINED-AUTH] Response status: %d\n", resp.StatusCode) if resp.StatusCode == 429 { - // Parse retry-after header if available retryAfter := resp.Header.Get("Retry-After") fmt.Printf("[DEFINED-AUTH] ⚠ Rate limited! Retry-After: %s\n", retryAfter) - if retryAfter != "" { - return "", fmt.Errorf("rate limited (429), retry after: %s", retryAfter) - } - return "", fmt.Errorf("rate limited (429), too many token requests - will retry later") + return "", fmt.Errorf("rate limited (429)") } if resp.StatusCode != 200 { - fmt.Printf("[DEFINED-AUTH] ❌ Unexpected status %d: %s\n", resp.StatusCode, string(respBody[:min(len(respBody), 100)])) - return "", fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody[:min(len(respBody), 100)])) + n := len(respBody) + if n > 100 { n = 100 } + fmt.Printf("[DEFINED-AUTH] ❌ Unexpected status %d: %s\n", resp.StatusCode, string(respBody[:n])) + return "", fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody[:n])) } var tokenResp DefinedTokenResponse @@ -183,7 +274,7 @@ func generateDefinedJWTToken(sessionCookie string) (string, error) { return "", fmt.Errorf("no token returned") } - fmt.Printf("[DEFINED-AUTH] ✅ JWT token generated successfully (length: %d)\n", len(tokenResp.Data.CreateApiTokens[0].Token)) + fmt.Printf("[DEFINED-AUTH] ✅ Token generated via un.defined.fi (length: %d)\n", len(tokenResp.Data.CreateApiTokens[0].Token)) return tokenResp.Data.CreateApiTokens[0].Token, nil } diff --git a/harnesses/aggregator-head-lag/cmd/script/head_lag_monitor.go b/harnesses/aggregator-head-lag/cmd/script/head_lag_monitor.go index cef588c4..a912795e 100644 --- a/harnesses/aggregator-head-lag/cmd/script/head_lag_monitor.go +++ b/harnesses/aggregator-head-lag/cmd/script/head_lag_monitor.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "log" + "net/http" + "net/url" "strings" "sync" "time" @@ -419,11 +421,19 @@ func connectAndMonitorCodex(config *Config, stopChan <-chan struct{}) error { return fmt.Errorf("failed to get JWT token: %w", err) } - log.Printf("[HEAD-LAG][CODEX] Step 2/4: Creating proxy dialer...") - dialer := getProxyDialerWithSubprotocols([]string{"graphql-transport-ws"}) + log.Printf("[HEAD-LAG][CODEX] Step 2/4: Creating direct dialer (no proxy — IP must match JWE origin)...") + dialer := &websocket.Dialer{ + Subprotocols: []string{"graphql-transport-ws"}, + HandshakeTimeout: 30 * time.Second, + } log.Printf("[HEAD-LAG][CODEX] Step 3/4: Connecting to wss://graph.codex.io/graphql...") - conn, resp, err := dialer.Dial("wss://graph.codex.io/graphql", nil) + cookieVal := url.QueryEscape(`{"token":"` + jwtToken + `"}`) + wsHeaders := http.Header{} + wsHeaders.Set("Cookie", "codex_token="+cookieVal) + wsHeaders.Set("Origin", "https://www.defined.fi") + wsHeaders.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + conn, resp, err := dialer.Dial("wss://graph.codex.io/graphql", wsHeaders) if err != nil { if resp != nil { return fmt.Errorf("dial failed (HTTP %d): %w", resp.StatusCode, err) @@ -439,12 +449,11 @@ func connectAndMonitorCodex(config *Config, stopChan <-chan struct{}) error { log.Printf("[HEAD-LAG][CODEX] Step 3/4: ✅ WebSocket connection established (IP check failed: %v)", err) } - // Connection init with JWT Bearer token - log.Printf("[HEAD-LAG][CODEX] Step 4/4: Sending connection_init with JWT...") + log.Printf("[HEAD-LAG][CODEX] Step 4/4: Sending connection_init (token len=%d)...", len(jwtToken)) initMsg := map[string]interface{}{ "type": "connection_init", "payload": map[string]interface{}{ - "Authorization": fmt.Sprintf("Bearer %s", jwtToken), + "Authorization": "Bearer " + jwtToken, }, } if err := conn.WriteJSON(initMsg); err != nil { diff --git a/harnesses/aggregator-head-lag/cmd/script/main.go b/harnesses/aggregator-head-lag/cmd/script/main.go index 84444379..a545d832 100644 --- a/harnesses/aggregator-head-lag/cmd/script/main.go +++ b/harnesses/aggregator-head-lag/cmd/script/main.go @@ -38,6 +38,11 @@ func main() { var wg sync.WaitGroup stopChan := make(chan struct{}) + // In-process JWE scraper: headless Chrome inside this container scrapes defined.fi + // every 5 min to keep the Codex token fresh. JWE expires in ~10 min, so 5 min gives + // a comfortable margin. Runs only if Chrome is installed (no-op otherwise). + startInProcessScraper(stopChan) + wg.Add(1) go func() { defer wg.Done() diff --git a/harnesses/aggregator-head-lag/cmd/script/scrape_session.go b/harnesses/aggregator-head-lag/cmd/script/scrape_session.go index 4607912c..d88746e6 100644 --- a/harnesses/aggregator-head-lag/cmd/script/scrape_session.go +++ b/harnesses/aggregator-head-lag/cmd/script/scrape_session.go @@ -42,13 +42,13 @@ func ScrapeDefinedSessionCookie() (string, error) { } for _, cookie := range cookieParams { - if cookie.Name == "session" { + if cookie.Name == "defined-attestation-token" { sessionCookie = cookie.Value return nil } } - return fmt.Errorf("session cookie not found") + return fmt.Errorf("defined-attestation-token cookie not found") }), ) diff --git a/harnesses/aggregator-head-lag/cmd/script/utls_codex.go b/harnesses/aggregator-head-lag/cmd/script/utls_codex.go new file mode 100644 index 00000000..a323bb64 --- /dev/null +++ b/harnesses/aggregator-head-lag/cmd/script/utls_codex.go @@ -0,0 +1,223 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/http/cookiejar" + "net/url" + "time" + + tls "github.com/refraction-networking/utls" +) + +func chromeH1Spec() tls.ClientHelloSpec { + return tls.ClientHelloSpec{ + TLSVersMax: tls.VersionTLS13, + TLSVersMin: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.GREASE_PLACEHOLDER, + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + }, + CompressionMethods: []byte{0x00}, + Extensions: tls.ShuffleChromeTLSExtensions([]tls.TLSExtension{ + &tls.UtlsGREASEExtension{}, + &tls.SNIExtension{}, + &tls.ExtendedMasterSecretExtension{}, + &tls.RenegotiationInfoExtension{Renegotiation: tls.RenegotiateOnceAsClient}, + &tls.SupportedCurvesExtension{[]tls.CurveID{ + tls.GREASE_PLACEHOLDER, tls.X25519, tls.CurveP256, tls.CurveP384, + }}, + &tls.SupportedPointsExtension{SupportedPoints: []byte{0x00}}, + &tls.SessionTicketExtension{}, + &tls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}, + &tls.StatusRequestExtension{}, + &tls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []tls.SignatureScheme{ + tls.ECDSAWithP256AndSHA256, tls.PSSWithSHA256, tls.PKCS1WithSHA256, + tls.ECDSAWithP384AndSHA384, tls.PSSWithSHA384, tls.PKCS1WithSHA384, + tls.PSSWithSHA512, tls.PKCS1WithSHA512, + }}, + &tls.SCTExtension{}, + &tls.KeyShareExtension{[]tls.KeyShare{ + {Group: tls.CurveID(tls.GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: tls.X25519}, + }}, + &tls.PSKKeyExchangeModesExtension{[]uint8{tls.PskModeDHE}}, + &tls.SupportedVersionsExtension{[]uint16{ + tls.GREASE_PLACEHOLDER, tls.VersionTLS13, tls.VersionTLS12, + }}, + &tls.UtlsCompressCertExtension{[]tls.CertCompressionAlgo{tls.CertCompressionBrotli}}, + &tls.UtlsGREASEExtension{}, + &tls.UtlsPaddingExtension{GetPaddingLen: tls.BoringPaddingStyle}, + }), + } +} + +func newUTLSClient() *http.Client { + jar, _ := cookiejar.New(nil) + return &http.Client{ + Timeout: 30 * time.Second, + Jar: jar, + Transport: &http.Transport{ + DisableKeepAlives: true, + ForceAttemptHTTP2: false, + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + host, _, _ := net.SplitHostPort(addr) + conn, err := (&net.Dialer{Timeout: 15 * time.Second}).DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + spec := chromeH1Spec() + uc := tls.UClient(conn, &tls.Config{ServerName: host}, tls.HelloCustom) + if err := uc.ApplyPreset(&spec); err != nil { + conn.Close() + return nil, err + } + if err := uc.HandshakeContext(ctx); err != nil { + conn.Close() + return nil, err + } + return uc, nil + }, + }, + } +} + +// tryUtlsLegacyAPI calls un.defined.fi/api (old UI, still alive) with createApiTokens mutation. +// No cookies or CSRF needed — one request, much simpler. Works from residential IPs. +// Try this first since it's a single POST with no page-visit prerequisite. +func tryUtlsLegacyAPI() (string, error) { + client := newUTLSClient() + + body, _ := json.Marshal(map[string]interface{}{ + "operationName": "CreateApiToken", + "query": "mutation CreateApiToken { createApiTokens(input: { count: 1 }) { token } }", + "variables": map[string]interface{}{}, + }) + req, _ := http.NewRequest("POST", "https://un.defined.fi/api", bytes.NewBuffer(body)) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + req.Header.Set("Origin", "https://un.defined.fi") + req.Header.Set("Referer", "https://un.defined.fi/") + req.Header.Set("Sec-Fetch-Site", "same-origin") + req.Header.Set("Sec-Fetch-Mode", "cors") + req.Header.Set("Sec-Fetch-Dest", "empty") + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("request failed: %w", err) + } + respBody, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP %d: %.100s", resp.StatusCode, string(respBody)) + } + + var parsed struct { + Data struct { + CreateApiTokens []struct { + Token string `json:"token"` + } `json:"createApiTokens"` + } `json:"data"` + } + if err := json.Unmarshal(respBody, &parsed); err != nil || len(parsed.Data.CreateApiTokens) == 0 || parsed.Data.CreateApiTokens[0].Token == "" { + return "", fmt.Errorf("no token in response: %.100s", string(respBody)) + } + return parsed.Data.CreateApiTokens[0].Token, nil +} + +// tryUtlsCodexToken uses Chrome TLS fingerprint spoofing to call www.defined.fi/api/codex/token. +// Requires a page visit first to get CSRF cookie. Fallback if tryUtlsLegacyAPI fails. +func tryUtlsCodexToken() (string, error) { + // Try the old un.defined.fi API first — no cookies or CSRF needed. + if tok, err := tryUtlsLegacyAPI(); err == nil && tok != "" { + return tok, nil + } + + client := newUTLSClient() + + req1, _ := http.NewRequest("GET", "https://www.defined.fi/", nil) + req1.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + req1.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req1.Header.Set("Accept-Language", "en-US,en;q=0.9") + req1.Header.Set("Sec-Fetch-Site", "none") + req1.Header.Set("Sec-Fetch-Mode", "navigate") + req1.Header.Set("Sec-Fetch-Dest", "document") + req1.Header.Set("Upgrade-Insecure-Requests", "1") + + resp1, err := client.Do(req1) + if err != nil { + return "", fmt.Errorf("page load failed: %w", err) + } + io.Copy(io.Discard, resp1.Body) + resp1.Body.Close() + + if resp1.StatusCode != 200 { + return "", fmt.Errorf("page load blocked (HTTP %d) — IP blocked by Vercel", resp1.StatusCode) + } + + u, _ := url.Parse("https://www.defined.fi/") + cookies := client.Jar.Cookies(u) + var csrfToken string + for _, c := range cookies { + if c.Name == "csrf-token" { + csrfToken = c.Value + } + } + if csrfToken == "" { + return "", fmt.Errorf("no csrf-token in page cookies") + } + + req2, _ := http.NewRequest("POST", "https://www.defined.fi/api/codex/token", bytes.NewBufferString("{}")) + req2.Header.Set("Accept", "application/json") + req2.Header.Set("Content-Type", "application/json") + req2.Header.Set("x-csrf-token", csrfToken) + req2.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req2.Header.Set("Accept-Language", "en-US,en;q=0.9") + req2.Header.Set("Origin", "https://www.defined.fi") + req2.Header.Set("Referer", "https://www.defined.fi/") + req2.Header.Set("Sec-Fetch-Site", "same-origin") + req2.Header.Set("Sec-Fetch-Mode", "cors") + req2.Header.Set("Sec-Fetch-Dest", "empty") + + resp2, err := client.Do(req2) + if err != nil { + return "", fmt.Errorf("codex/token request failed: %w", err) + } + body2, _ := io.ReadAll(resp2.Body) + resp2.Body.Close() + + if resp2.StatusCode != 200 { + return "", fmt.Errorf("codex/token HTTP %d: %.100s", resp2.StatusCode, string(body2)) + } + + var parsed struct { + Token string `json:"token"` + } + if err := json.Unmarshal(body2, &parsed); err != nil || parsed.Token == "" { + return "", fmt.Errorf("no token in response: %.100s", string(body2)) + } + + return parsed.Token, nil +} diff --git a/harnesses/aggregator-head-lag/cmd/test-utls/main.go b/harnesses/aggregator-head-lag/cmd/test-utls/main.go new file mode 100644 index 00000000..cbb8adbc --- /dev/null +++ b/harnesses/aggregator-head-lag/cmd/test-utls/main.go @@ -0,0 +1,261 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/http/cookiejar" + "net/url" + "os" + "time" + + tls "github.com/refraction-networking/utls" +) + +func chromeH1Spec() tls.ClientHelloSpec { + return tls.ClientHelloSpec{ + TLSVersMax: tls.VersionTLS13, + TLSVersMin: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.GREASE_PLACEHOLDER, + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + }, + CompressionMethods: []byte{0x00}, + Extensions: tls.ShuffleChromeTLSExtensions([]tls.TLSExtension{ + &tls.UtlsGREASEExtension{}, + &tls.SNIExtension{}, + &tls.ExtendedMasterSecretExtension{}, + &tls.RenegotiationInfoExtension{Renegotiation: tls.RenegotiateOnceAsClient}, + &tls.SupportedCurvesExtension{[]tls.CurveID{ + tls.GREASE_PLACEHOLDER, tls.X25519, tls.CurveP256, tls.CurveP384, + }}, + &tls.SupportedPointsExtension{SupportedPoints: []byte{0x00}}, + &tls.SessionTicketExtension{}, + &tls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}, + &tls.StatusRequestExtension{}, + &tls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []tls.SignatureScheme{ + tls.ECDSAWithP256AndSHA256, tls.PSSWithSHA256, tls.PKCS1WithSHA256, + tls.ECDSAWithP384AndSHA384, tls.PSSWithSHA384, tls.PKCS1WithSHA384, + tls.PSSWithSHA512, tls.PKCS1WithSHA512, + }}, + &tls.SCTExtension{}, + &tls.KeyShareExtension{[]tls.KeyShare{ + {Group: tls.CurveID(tls.GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: tls.X25519}, + }}, + &tls.PSKKeyExchangeModesExtension{[]uint8{tls.PskModeDHE}}, + &tls.SupportedVersionsExtension{[]uint16{ + tls.GREASE_PLACEHOLDER, tls.VersionTLS13, tls.VersionTLS12, + }}, + &tls.UtlsCompressCertExtension{[]tls.CertCompressionAlgo{tls.CertCompressionBrotli}}, + &tls.UtlsGREASEExtension{}, + &tls.UtlsPaddingExtension{GetPaddingLen: tls.BoringPaddingStyle}, + }), + } +} + +func newUTLSClient() *http.Client { + jar, _ := cookiejar.New(nil) + return &http.Client{ + Timeout: 30 * time.Second, + Jar: jar, + Transport: &http.Transport{ + DisableKeepAlives: true, + ForceAttemptHTTP2: false, + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + host, _, _ := net.SplitHostPort(addr) + conn, err := (&net.Dialer{Timeout: 15 * time.Second}).DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + spec := chromeH1Spec() + uc := tls.UClient(conn, &tls.Config{ServerName: host}, tls.HelloCustom) + if err := uc.ApplyPreset(&spec); err != nil { + conn.Close() + return nil, err + } + if err := uc.HandshakeContext(ctx); err != nil { + conn.Close() + return nil, err + } + return uc, nil + }, + }, + } +} + +func main() { + mode := os.Getenv("MODE") + + switch mode { + case "fetch-html": + // Fetch URL via utls, print body to stdout + target := os.Getenv("URL") + if target == "" { + target = "https://www.defined.fi/" + } + client := newUTLSClient() + req, _ := http.NewRequest("GET", target, nil) + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + resp, err := client.Do(req) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + os.Exit(1) + } + defer resp.Body.Close() + fmt.Fprintf(os.Stderr, "HTTP %d\n", resp.StatusCode) + io.Copy(os.Stdout, resp.Body) + + case "scrape": + // Step 1 only: GET defined.fi, print cookies + client := newUTLSClient() + req, _ := http.NewRequest("GET", "https://www.defined.fi/", nil) + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + req.Header.Set("Sec-Fetch-Site", "none") + req.Header.Set("Sec-Fetch-Mode", "navigate") + req.Header.Set("Sec-Fetch-Dest", "document") + req.Header.Set("Upgrade-Insecure-Requests", "1") + resp, err := client.Do(req) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + os.Exit(1) + } + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + if resp.StatusCode != 200 { + fmt.Fprintf(os.Stderr, "ERROR: HTTP %d\n", resp.StatusCode) + os.Exit(1) + } + u, _ := url.Parse("https://www.defined.fi/") + for _, c := range client.Jar.Cookies(u) { + fmt.Printf("%s=%s\n", c.Name, c.Value) + } + + case "mint-utls": + // Step 2 only via utls: POST /api/codex/token with pre-obtained cookies + attestation := os.Getenv("ATTESTATION") + csrf := os.Getenv("CSRF") + if attestation == "" || csrf == "" { + fmt.Fprintln(os.Stderr, "Need ATTESTATION and CSRF env vars") + os.Exit(1) + } + fmt.Printf("Calling /api/codex/token via utls (Chrome fingerprint)...\n") + client := newUTLSClient() + // Inject cookies into jar + u, _ := url.Parse("https://www.defined.fi/") + client.Jar.SetCookies(u, []*http.Cookie{ + {Name: "defined-attestation-token", Value: attestation}, + {Name: "csrf-token", Value: csrf}, + }) + req, _ := http.NewRequest("POST", "https://www.defined.fi/api/codex/token", bytes.NewBufferString("{}")) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-csrf-token", csrf) + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + req.Header.Set("Origin", "https://www.defined.fi") + req.Header.Set("Referer", "https://www.defined.fi/") + req.Header.Set("Sec-Fetch-Site", "same-origin") + req.Header.Set("Sec-Fetch-Mode", "cors") + req.Header.Set("Sec-Fetch-Dest", "empty") + resp, err := client.Do(req) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + os.Exit(1) + } + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + fmt.Printf("HTTP %d\n", resp.StatusCode) + if resp.StatusCode == 200 { + var parsed struct{ Token string `json:"token"` } + if err := json.Unmarshal(body, &parsed); err == nil && parsed.Token != "" { + fmt.Printf("✅ Got JWE (len=%d)\n", len(parsed.Token)) + fmt.Printf("CODEX_TOKEN=%s\n", parsed.Token) + return + } + } + n := len(body) + if n > 200 { + n = 200 + } + fmt.Printf("❌ Body: %s\n", string(body[:n])) + os.Exit(1) + + default: + // Full flow: scrape + mint on this machine + client := newUTLSClient() + req1, _ := http.NewRequest("GET", "https://www.defined.fi/", nil) + req1.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + req1.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req1.Header.Set("Accept-Language", "en-US,en;q=0.9") + req1.Header.Set("Sec-Fetch-Site", "none") + req1.Header.Set("Sec-Fetch-Mode", "navigate") + req1.Header.Set("Sec-Fetch-Dest", "document") + req1.Header.Set("Upgrade-Insecure-Requests", "1") + resp1, err := client.Do(req1) + if err != nil { + fmt.Printf("Page load failed: %v\n", err) + return + } + io.Copy(io.Discard, resp1.Body) + resp1.Body.Close() + fmt.Printf("Page: HTTP %d\n", resp1.StatusCode) + u, _ := url.Parse("https://www.defined.fi/") + cookies := client.Jar.Cookies(u) + var attestation, csrf string + for _, c := range cookies { + switch c.Name { + case "defined-attestation-token": + attestation = c.Value + case "csrf-token": + csrf = c.Value + } + } + fmt.Printf("attestation len=%d, csrf len=%d\n", len(attestation), len(csrf)) + req2, _ := http.NewRequest("POST", "https://www.defined.fi/api/codex/token", bytes.NewBufferString("{}")) + req2.Header.Set("Accept", "application/json") + req2.Header.Set("Content-Type", "application/json") + req2.Header.Set("x-csrf-token", csrf) + req2.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req2.Header.Set("Origin", "https://www.defined.fi") + req2.Header.Set("Referer", "https://www.defined.fi/") + req2.Header.Set("Sec-Fetch-Site", "same-origin") + req2.Header.Set("Sec-Fetch-Mode", "cors") + resp2, err := client.Do(req2) + if err != nil { + fmt.Printf("API failed: %v\n", err) + return + } + body, _ := io.ReadAll(resp2.Body) + resp2.Body.Close() + fmt.Printf("API: HTTP %d\n", resp2.StatusCode) + if resp2.StatusCode == 200 { + var parsed struct{ Token string `json:"token"` } + if err := json.Unmarshal(body, &parsed); err == nil && parsed.Token != "" { + fmt.Printf("✅ JWE len=%d\n", len(parsed.Token)) + } + } + } +} diff --git a/harnesses/aggregator-head-lag/go.mod b/harnesses/aggregator-head-lag/go.mod index ed17b543..d6ccd878 100644 --- a/harnesses/aggregator-head-lag/go.mod +++ b/harnesses/aggregator-head-lag/go.mod @@ -10,6 +10,7 @@ require ( ) require ( + github.com/andybalholm/brotli v1.0.6 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chromedp/sysutil v1.1.0 // indirect @@ -17,12 +18,15 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/refraction-networking/utls v1.8.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/sys v0.35.0 // indirect google.golang.org/protobuf v1.36.8 // indirect ) diff --git a/harnesses/aggregator-head-lag/go.sum b/harnesses/aggregator-head-lag/go.sum index f594bc89..6379815d 100644 --- a/harnesses/aggregator-head-lag/go.sum +++ b/harnesses/aggregator-head-lag/go.sum @@ -1,3 +1,5 @@ +github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= +github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 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= @@ -47,6 +49,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= +github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -55,6 +59,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/harnesses/aggregator-latency-benchmark/cmd/script/defined_auth.go b/harnesses/aggregator-latency-benchmark/cmd/script/defined_auth.go index 716f05f0..717e75ad 100644 --- a/harnesses/aggregator-latency-benchmark/cmd/script/defined_auth.go +++ b/harnesses/aggregator-latency-benchmark/cmd/script/defined_auth.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "os" "strings" "sync" "time" @@ -57,8 +58,12 @@ func decodeJWTExpiration(token string) (time.Time, error) { return time.Unix(claims.Exp, 0), nil } -// GetDefinedJWTToken returns a cached JWT token or generates a new one if expired +// GetDefinedJWTToken returns a cached JWT token or generates a new one if expired. +// If CODEX_JWT env var is set, it is returned directly (bypasses session-cookie flow). func GetDefinedJWTToken(sessionCookie string) (string, error) { + if jwt := os.Getenv("CODEX_JWT"); jwt != "" { + return jwt, nil + } globalTokenCache.mu.RLock() // Check if we have a valid cached token @@ -144,7 +149,7 @@ func generateDefinedJWTToken(sessionCookie string) (string, error) { req.Header.Set("sec-fetch-dest", "empty") req.Header.Set("sec-fetch-mode", "cors") req.Header.Set("sec-fetch-site", "same-origin") - req.AddCookie(&http.Cookie{Name: "session", Value: sessionCookie}) + req.AddCookie(&http.Cookie{Name: "defined-attestation-token", Value: sessionCookie}) fmt.Println("[DEFINED-AUTH] Sending POST request to https://www.defined.fi/api...") resp, err := client.Do(req) diff --git a/harnesses/aggregator-latency-benchmark/cmd/script/scrape_session.go b/harnesses/aggregator-latency-benchmark/cmd/script/scrape_session.go index 4607912c..26d3f770 100644 --- a/harnesses/aggregator-latency-benchmark/cmd/script/scrape_session.go +++ b/harnesses/aggregator-latency-benchmark/cmd/script/scrape_session.go @@ -42,7 +42,7 @@ func ScrapeDefinedSessionCookie() (string, error) { } for _, cookie := range cookieParams { - if cookie.Name == "session" { + if cookie.Name == "defined-attestation-token" { sessionCookie = cookie.Value return nil } diff --git a/harnesses/apps/Dockerfile b/harnesses/apps/Dockerfile new file mode 100644 index 00000000..a3752b65 --- /dev/null +++ b/harnesses/apps/Dockerfile @@ -0,0 +1,15 @@ +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 +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..95aecfec --- /dev/null +++ b/harnesses/apps/cmd/api/main.go @@ -0,0 +1,180 @@ +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"}, + "gmx-v2:arbitrum": {Name: "GMX v2 (Arbitrum)", Slug: "gmx", Category: "perps"}, + "gmx-v2:avalanche": {Name: "GMX v2 (Avalanche)", Slug: "gmx", Category: "perps"}, + "gains-trade:arbitrum": {Name: "Gains.trade", Slug: "gains-trade", Category: "perps"}, + "drift:solana": {Name: "Drift", Slug: "drift", Category: "perps"}, + "jupiter-perps:solana": {Name: "Jupiter Perps", Slug: "jupiter-perps", Category: "perps"}, + "vertex:arbitrum": {Name: "Vertex", Slug: "vertex", 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 '2 days' 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", "treasury": + 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/harnesses/apps/cmd/collector/main.go b/harnesses/apps/cmd/collector/main.go new file mode 100644 index 00000000..72d9c4aa --- /dev/null +++ b/harnesses/apps/cmd/collector/main.go @@ -0,0 +1,141 @@ +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() + hl := rest.NewHyperliquid() + gmxArb := rest.NewGMXv2Arbitrum() + gmxAvax := rest.NewGMXv2Avalanche() + drift := rest.NewDeFiLlama("drift-trade") + jupiterPerps := rest.NewDeFiLlama("jupiter-perpetual-exchange") + vertex := rest.NewDeFiLlama("vertex-perps") + + gainsWS := rest.NewGainsTradeWS() + + // WebSocket collector: real-time per-trade fees from gTrade first-party backend. + // Runs as a persistent goroutine; reconnects on any error. + go func() { + for { + out := make(chan spec.FeeEvent, 500) + after := time.Now().UTC().Truncate(24 * time.Hour) + go func() { + defer close(out) + if err := gainsWS.Stream(ctx, "gains-trade:arbitrum", after, out); err != nil && ctx.Err() == nil { + log.Printf("gains-trade WS: %v", err) + } + }() + for e := range out { + if err := db.UpsertEvents(ctx, []spec.FeeEvent{e}); err != nil { + log.Printf("gains-trade WS upsert: %v", err) + } + } + if ctx.Err() != nil { + return + } + time.Sleep(10 * time.Second) + } + }() + + for { + 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) + } + if err := runCollector(ctx, db, gmxArb, "gmx-v2:arbitrum"); err != nil { + log.Printf("gmx-v2:arbitrum collector error: %v", err) + } + if err := runCollector(ctx, db, gmxAvax, "gmx-v2:avalanche"); err != nil { + log.Printf("gmx-v2:avalanche collector error: %v", err) + } + if err := runCollector(ctx, db, drift, "drift:solana"); err != nil { + log.Printf("drift collector error: %v", err) + } + if err := runCollector(ctx, db, jupiterPerps, "jupiter-perps:solana"); err != nil { + log.Printf("jupiter-perps collector error: %v", err) + } + if err := runCollector(ctx, db, vertex, "vertex:arbitrum"); err != nil { + log.Printf("vertex collector error: %v", err) + } + time.Sleep(60 * time.Second) + } +} + +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), + 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..cc44a9f4 --- /dev/null +++ b/harnesses/apps/cmd/materializer/main.go @@ -0,0 +1,77 @@ +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", + "hyperliquid:hypercore", + "gmx-v2:arbitrum", + "gmx-v2:avalanche", + "gains-trade:arbitrum", + "drift:solana", + "jupiter-perps:solana", + "vertex:arbitrum", + } + + 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..1dd64ab2 --- /dev/null +++ b/harnesses/apps/go.mod @@ -0,0 +1,27 @@ +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/gorilla/websocket v1.5.3 // 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..65512705 --- /dev/null +++ b/harnesses/apps/go.sum @@ -0,0 +1,54 @@ +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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/defillama_generic.go b/harnesses/apps/internal/collect/rest/defillama_generic.go new file mode 100644 index 00000000..444bb478 --- /dev/null +++ b/harnesses/apps/internal/collect/rest/defillama_generic.go @@ -0,0 +1,189 @@ +package rest + +import ( + "context" + "encoding/json" + "fmt" + "math" + "net/http" + "sort" + "time" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +// DeFiLlamaCollector fetches daily fees and revenue from the DeFiLlama public +// API for any protocol slug and emits treasury + LP events per day. +// No API key required. +type DeFiLlamaCollector struct { + client *http.Client + slug string +} + +func NewDeFiLlama(slug string) *DeFiLlamaCollector { + return &DeFiLlamaCollector{ + client: &http.Client{Timeout: 30 * time.Second}, + slug: slug, + } +} + +func (c *DeFiLlamaCollector) Name() string { return "defillama:" + c.slug } + +type llamaChartResp struct { + TotalDataChart [][]json.RawMessage `json:"totalDataChart"` +} + +type llamaChartEntry struct { + Ts int64 + Value float64 +} + +func (c *DeFiLlamaCollector) Collect( + ctx context.Context, + deploymentID string, + from, to spec.Cursor, + out chan<- spec.FeeEvent, +) (spec.Cursor, error) { + cursor := from + + feesURL := "https://api.llama.fi/summary/fees/" + c.slug + "?dataType=dailyFees" + revURL := "https://api.llama.fi/summary/fees/" + c.slug + "?dataType=dailyRevenue" + + fees, err := c.fetchChart(ctx, feesURL) + if err != nil { + return cursor, fmt.Errorf("%s fees: %w", c.slug, err) + } + + revMap := map[int64]float64{} + revAvailable := true + if revs, err := c.fetchChart(ctx, revURL); err != nil { + revAvailable = false + } else { + for _, e := range revs { + revMap[e.Ts] = e.Value + } + } + + today := time.Now().UTC().Truncate(24 * time.Hour) + + for _, e := range fees { + if e.Value <= 0 { + continue + } + ts := time.Unix(e.Ts, 0).UTC() + if !ts.Before(today) { + continue + } + h := uint64(e.Ts) + if h <= from.Height { + continue + } + + day := ts.Format("2006-01-02") + grossUSD := e.Value + + if !revAvailable || revMap[e.Ts] == 0 { + // Revenue split unknown — emit gross as burn. + micro := int64(math.Round(grossUSD * 1e6)) + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("llama:%s:burn:%s", c.slug, day), + Ts: ts, + Height: h, + Component: "position_fee", + Beneficiary: "burn", + Token: "USD", + AmountRaw: fmt.Sprintf("%d", micro), + Decimals: 6, + Market: "all", + Finality: spec.FinalityFinal, + Source: c.Name(), + } + } else { + revUSD := revMap[e.Ts] + if revUSD > grossUSD { + revUSD = grossUSD + } + lpUSD := grossUSD - revUSD + + if revUSD > 0 { + micro := int64(math.Round(revUSD * 1e6)) + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("llama:%s:treasury:%s", c.slug, day), + Ts: ts, + Height: h, + Component: "position_fee", + Beneficiary: "treasury", + Token: "USD", + AmountRaw: fmt.Sprintf("%d", micro), + Decimals: 6, + Market: "all", + Finality: spec.FinalityFinal, + Source: c.Name(), + } + } + if lpUSD > 0 { + micro := int64(math.Round(lpUSD * 1e6)) + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("llama:%s:lp:%s", c.slug, day), + Ts: ts, + Height: h, + Component: "position_fee", + Beneficiary: "lp", + Token: "USD", + AmountRaw: fmt.Sprintf("%d", micro), + Decimals: 6, + Market: "all", + Finality: spec.FinalityFinal, + Source: c.Name(), + } + } + } + + cursor = spec.Cursor{Height: h, Ts: ts, Finalized: true} + } + + return cursor, nil +} + +func (c *DeFiLlamaCollector) fetchChart(ctx context.Context, url string) ([]llamaChartEntry, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, 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", resp.StatusCode) + } + + var raw llamaChartResp + if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { + return nil, fmt.Errorf("decode: %w", err) + } + + var out []llamaChartEntry + for _, row := range raw.TotalDataChart { + if len(row) < 2 { + continue + } + var ts int64 + if err := json.Unmarshal(row[0], &ts); err != nil { + continue + } + var val float64 + if err := json.Unmarshal(row[1], &val); err != nil { + continue + } + out = append(out, llamaChartEntry{Ts: ts, Value: val}) + } + sort.Slice(out, func(i, j int) bool { return out[i].Ts < out[j].Ts }) + return out, nil +} diff --git a/harnesses/apps/internal/collect/rest/dydx.go b/harnesses/apps/internal/collect/rest/dydx.go new file mode 100644 index 00000000..c470568d --- /dev/null +++ b/harnesses/apps/internal/collect/rest/dydx.go @@ -0,0 +1,188 @@ +package rest + +import ( + "context" + "encoding/json" + "fmt" + "math" + "net/http" + "sort" + "strconv" + "time" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +const dydxIndexer = "https://indexer.dydx.trade/v4" + +// dYdX v4 effective blended taker fee rate. +// Standard tier: 5bps. VIP tiers (which dominate volume): 2-4bps. +// Empirical blended rate ≈ 3.5bps (large traders ~60% of volume at 2-3bps, +// retail ~40% at 5bps). Using 5bps overstates fees by ~30%. +const dydxTakerFeeBps = 3.5 + +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-volume-fees" } + +type dydxCandle struct { + StartedAt string `json:"startedAt"` // "2026-08-09T00:00:00.000Z" + USDVolume string `json:"usdVolume"` +} + +func (c *DyDXCollector) Collect( + ctx context.Context, + deploymentID string, + from, to spec.Cursor, + out chan<- spec.FeeEvent, +) (spec.Cursor, error) { + cursor := from + + // Step 1: fetch all markets, keep those with any 24h volume. + markets, err := c.fetchActiveMarkets(ctx) + if err != nil { + return cursor, fmt.Errorf("dydx: markets: %w", err) + } + + // Step 2: fetch 90d of daily candles for each active market. + // Aggregate usdVolume per calendar day (UTC midnight). + dailyVol := map[string]float64{} // "YYYY-MM-DD" -> total USD volume + + for _, ticker := range markets { + candles, err := c.fetchCandles(ctx, ticker, 1000) + if err != nil { + // Non-fatal: skip this market if it fails. + continue + } + for _, candle := range candles { + day := candle.StartedAt[:10] // "YYYY-MM-DD" + vol, err := strconv.ParseFloat(candle.USDVolume, 64) + if err != nil || vol <= 0 { + continue + } + dailyVol[day] += vol + } + } + + // Step 3: sort days and emit fee events. + days := make([]string, 0, len(dailyVol)) + for d := range dailyVol { + days = append(days, d) + } + sort.Strings(days) + + today := time.Now().UTC().Format("2006-01-02") + + for _, day := range days { + if day >= today { + continue // skip incomplete current day + } + + ts, err := time.Parse("2006-01-02", day) + if err != nil { + continue + } + ts = ts.UTC() + h := uint64(ts.Unix()) + + if h <= from.Height { + continue + } + + vol := dailyVol[day] + if vol <= 0 { + continue + } + + // fees = volume × (takerFeeBps / 10000) + feesUSD := vol * dydxTakerFeeBps / 10000.0 + amountMicro := int64(math.Round(feesUSD * 1e6)) + + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("dydx:vol-fee:%s", day), + Ts: ts, + Height: h, + Component: "taker_fee", + Beneficiary: "burn", + Token: "USDC", + AmountRaw: fmt.Sprintf("%d", amountMicro), + Decimals: 6, + Market: "all", + Finality: spec.FinalityFinal, + Source: "dydx-indexer-volume", + Meta: map[string]string{ + "usd_volume": fmt.Sprintf("%.2f", vol), + "fee_rate_bps": fmt.Sprintf("%.1f", dydxTakerFeeBps), + "note": "volume * 5bps taker fee; maker fee = 0 on dYdX v4", + }, + } + + cursor = spec.Cursor{Height: h + 86400, Ts: ts, Finalized: true} + } + + return cursor, nil +} + +func (c *DyDXCollector) fetchActiveMarkets(ctx context.Context) ([]string, error) { + url := fmt.Sprintf("%s/perpetualMarkets?limit=500", 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, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP %d from %s", resp.StatusCode, url) + } + + var result struct { + Markets map[string]json.RawMessage `json:"markets"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("decode: %w", err) + } + + tickers := make([]string, 0, len(result.Markets)) + for ticker := range result.Markets { + tickers = append(tickers, ticker) + } + return tickers, nil +} + +func (c *DyDXCollector) fetchCandles(ctx context.Context, ticker string, days int) ([]dydxCandle, error) { + url := fmt.Sprintf("%s/candles/perpetualMarkets/%s?resolution=1DAY&limit=%d", dydxIndexer, ticker, days) + 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, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP %d", resp.StatusCode) + } + + var result struct { + Candles []dydxCandle `json:"candles"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("decode: %w", err) + } + return result.Candles, nil +} diff --git a/harnesses/apps/internal/collect/rest/gains_trade.go b/harnesses/apps/internal/collect/rest/gains_trade.go new file mode 100644 index 00000000..c56635e5 --- /dev/null +++ b/harnesses/apps/internal/collect/rest/gains_trade.go @@ -0,0 +1,181 @@ +package rest + +import ( + "context" + "encoding/json" + "fmt" + "math" + "net/http" + "sort" + "time" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +// DeFiLlama public fees API — exposes Gains Network daily fees computed from +// the first-party Dune table (dune.gains.result_g_trade_stats_defi_llama). +// No API key required. Covers Arbitrum, Polygon, Base, MegaETH, ApeChain. +const gainsLlamaFees = "https://api.llama.fi/summary/fees/gains-network?dataType=dailyFees" +const gainsLlamaRevenue = "https://api.llama.fi/summary/fees/gains-network?dataType=dailyRevenue" + +type GainsTradeCollector struct { + client *http.Client +} + +func NewGainsTrade() *GainsTradeCollector { + return &GainsTradeCollector{client: &http.Client{Timeout: 30 * time.Second}} +} + +func (c *GainsTradeCollector) Name() string { return "gains-trade-defillama" } + +// llamaBreakdownEntry is one row from totalDataChartBreakdown. +// [timestamp, {chain: {protocol: amount_usd}}] +type llamaBreakdownEntry struct { + Ts int64 + Chains map[string]map[string]float64 +} + +func (c *GainsTradeCollector) Collect( + ctx context.Context, + deploymentID string, + from, to spec.Cursor, + out chan<- spec.FeeEvent, +) (spec.Cursor, error) { + cursor := from + + fees, err := c.fetchBreakdown(ctx, gainsLlamaFees) + if err != nil { + return cursor, fmt.Errorf("gains-trade fees: %w", err) + } + revenues, err := c.fetchBreakdown(ctx, gainsLlamaRevenue) + if err != nil { + return cursor, fmt.Errorf("gains-trade revenue: %w", err) + } + + // Build revenue lookup by timestamp. + revByTs := map[int64]float64{} + for _, e := range revenues { + revByTs[e.Ts] = e.Chains["Arbitrum"]["Gains Network"] + } + + today := time.Now().UTC().Truncate(24 * time.Hour) + // WS collector (gains_trade_ws.go) handles real-time data from this date onwards. + // DeFiLlama only backfills history before the cutoff to avoid double-counting. + wsHandoverDate := time.Date(2026, 8, 10, 0, 0, 0, 0, time.UTC) + + for _, e := range fees { + arbFees := e.Chains["Arbitrum"]["Gains Network"] + if arbFees <= 0 { + continue + } + + ts := time.Unix(e.Ts, 0).UTC() + if !ts.Before(today) || !ts.Before(wsHandoverDate) { + continue // skip current day and days covered by WS collector + } + h := uint64(e.Ts) + if h <= from.Height { + continue + } + + arbRev := revByTs[e.Ts] + if arbRev > arbFees { + arbRev = arbFees + } + lpFees := arbFees - arbRev + + // Treasury/token-holders: GNS stakers + governance (DeFiLlama "revenue") + treasuryMicro := int64(math.Round(arbRev * 1e6)) + // LPs: gToken vault + referrals + bots + borrowing (DeFiLlama "supply-side") + lpMicro := int64(math.Round(lpFees * 1e6)) + + day := ts.Format("2006-01-02") + + if treasuryMicro > 0 { + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("gains:arb:treasury:%s", day), + Ts: ts, + Height: h, + Component: "position_fee", + Beneficiary: "treasury", + Token: "USD", + AmountRaw: fmt.Sprintf("%d", treasuryMicro), + Decimals: 6, + Market: "all", + Finality: spec.FinalityFinal, + Source: c.Name(), + Meta: map[string]string{ + "gross_usd": fmt.Sprintf("%.2f", arbFees), + "source_url": gainsLlamaFees, + }, + } + } + if lpMicro > 0 { + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("gains:arb:lp:%s", day), + Ts: ts, + Height: h, + Component: "position_fee", + Beneficiary: "lp", + Token: "USD", + AmountRaw: fmt.Sprintf("%d", lpMicro), + Decimals: 6, + Market: "all", + Finality: spec.FinalityFinal, + Source: c.Name(), + } + } + + cursor = spec.Cursor{Height: h, Ts: ts, Finalized: true} + } + + return cursor, nil +} + +// llamaResp is the shape of the DeFiLlama summary/fees endpoint. +type llamaResp struct { + TotalDataChartBreakdown [][]json.RawMessage `json:"totalDataChartBreakdown"` +} + +func (c *GainsTradeCollector) fetchBreakdown(ctx context.Context, url string) ([]llamaBreakdownEntry, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, 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 DeFiLlama", resp.StatusCode) + } + + var raw llamaResp + if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { + return nil, fmt.Errorf("decode: %w", err) + } + + var out []llamaBreakdownEntry + for _, row := range raw.TotalDataChartBreakdown { + if len(row) < 2 { + continue + } + var ts int64 + if err := json.Unmarshal(row[0], &ts); err != nil { + continue + } + var chains map[string]map[string]float64 + if err := json.Unmarshal(row[1], &chains); err != nil { + continue + } + out = append(out, llamaBreakdownEntry{Ts: ts, Chains: chains}) + } + + sort.Slice(out, func(i, j int) bool { return out[i].Ts < out[j].Ts }) + return out, nil +} diff --git a/harnesses/apps/internal/collect/rest/gains_trade_ws.go b/harnesses/apps/internal/collect/rest/gains_trade_ws.go new file mode 100644 index 00000000..2b95e249 --- /dev/null +++ b/harnesses/apps/internal/collect/rest/gains_trade_ws.go @@ -0,0 +1,232 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "math" + "math/big" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gorilla/websocket" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +const gainsWSS = "wss://backend-arbitrum.gains.trade" +const gainsArbRPC = "https://arb1.arbitrum.io/rpc" + +// topic0 hashes extracted from on-chain receipts (Gains Diamond, Arbitrum) +const ( + topicGovFeeCharged = "0xeb561f0609b402569e8a7e8fe9d4f408b92c96fb83001b2cd78fd55c29bbbac3" + topicGTokenFeeCharged = "0xfe4ab97508a97bb85ad1e2680662e58549e51982d965eed4ef6d7fcd4cc4295f" +) + +// collateral token decimals on Arbitrum (Gains collateralIndex → decimals) +var gainsCollDecimals = map[int]int{ + 1: 6, // USDC.e + 2: 18, // DAI + 3: 6, // USDC (native) + 4: 18, // ETH +} + +// GainsTradeWSCollector streams real-time trade events from the gTrade first-party +// WebSocket and resolves exact fee amounts via eth_getTransactionReceipt. +// arb1.arbitrum.io allows receipt lookups without auth; no API key needed. +type GainsTradeWSCollector struct { + client *http.Client +} + +func NewGainsTradeWS() *GainsTradeWSCollector { + return &GainsTradeWSCollector{client: &http.Client{Timeout: 10 * time.Second}} +} + +type wsTradeMsg struct { + Name string `json:"name"` + Date string `json:"date"` + TX string `json:"tx"` + CollateralIndex string `json:"collateralIndex"` + CollateralPriceUsd float64 `json:"collateralPriceUsd"` +} + +// Stream connects to the gTrade Arbitrum WebSocket and sends fee events for every +// trade that settled after `after`. It returns on connection error; the caller +// is responsible for reconnecting. +func (c *GainsTradeWSCollector) Stream(ctx context.Context, deploymentID string, after time.Time, out chan<- spec.FeeEvent) error { + dialer := websocket.Dialer{HandshakeTimeout: 10 * time.Second} + conn, _, err := dialer.DialContext(ctx, gainsWSS, nil) + if err != nil { + return fmt.Errorf("dial: %w", err) + } + defer conn.Close() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + _, raw, err := conn.ReadMessage() + if err != nil { + return fmt.Errorf("read: %w", err) + } + + var msg wsTradeMsg + if err := json.Unmarshal(raw, &msg); err != nil { + continue + } + if msg.Name != "new-trade-history" || msg.TX == "" { + continue + } + + ts, err := parseGainsDate(msg.Date) + if err != nil || !ts.After(after) { + continue + } + if msg.CollateralPriceUsd <= 0 { + msg.CollateralPriceUsd = 1.0 + } + + collIdx := 3 + if n, err := strconv.Atoi(msg.CollateralIndex); err == nil { + collIdx = n + } + decimals := gainsCollDecimals[collIdx] + if decimals == 0 { + decimals = 6 + } + + govMicro, lpMicro, err := c.fetchFees(ctx, msg.TX, decimals, msg.CollateralPriceUsd) + if err != nil || (govMicro == 0 && lpMicro == 0) { + continue + } + + h := uint64(ts.Unix()) + txKey := strings.ToLower(msg.TX) + + if govMicro > 0 { + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: "gains:arb:treasury:tx:" + txKey, + Ts: ts, + Height: h, + Component: "position_fee", + Beneficiary: "treasury", + Token: "USD", + AmountRaw: strconv.FormatInt(govMicro, 10), + Decimals: 6, + Market: "all", + Finality: spec.FinalityFinal, + Source: "gains-trade-ws", + } + } + if lpMicro > 0 { + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: "gains:arb:lp:tx:" + txKey, + Ts: ts, + Height: h, + Component: "position_fee", + Beneficiary: "lp", + Token: "USD", + AmountRaw: strconv.FormatInt(lpMicro, 10), + Decimals: 6, + Market: "all", + Finality: spec.FinalityFinal, + Source: "gains-trade-ws", + } + } + } +} + +func parseGainsDate(s string) (time.Time, error) { + for _, layout := range []string{"2006-01-02T15:04:05.999Z", time.RFC3339} { + if t, err := time.Parse(layout, s); err == nil { + return t, nil + } + } + return time.Time{}, fmt.Errorf("unparseable date: %s", s) +} + +type arbRPCReq struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params []any `json:"params"` + ID int `json:"id"` +} + +type arbRPCResp struct { + Result *struct { + Logs []struct { + Topics []string `json:"topics"` + Data string `json:"data"` + } `json:"logs"` + } `json:"result"` + Error *struct{ Message string } `json:"error"` +} + +func (c *GainsTradeWSCollector) fetchFees(ctx context.Context, txHash string, collDecimals int, collPriceUSD float64) (govMicro, lpMicro int64, err error) { + body, _ := json.Marshal(arbRPCReq{ + JSONRPC: "2.0", + Method: "eth_getTransactionReceipt", + Params: []any{txHash}, + ID: 1, + }) + + req, _ := http.NewRequestWithContext(ctx, http.MethodPost, gainsArbRPC, bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.client.Do(req) + if err != nil { + return 0, 0, err + } + defer resp.Body.Close() + + var rpc arbRPCResp + if err := json.NewDecoder(resp.Body).Decode(&rpc); err != nil { + return 0, 0, err + } + if rpc.Error != nil { + return 0, 0, fmt.Errorf("rpc: %s", rpc.Error.Message) + } + if rpc.Result == nil { + return 0, 0, fmt.Errorf("no receipt: %s", txHash) + } + + divisor := new(big.Float).SetFloat64(math.Pow10(collDecimals)) + + for _, log := range rpc.Result.Logs { + if len(log.Topics) == 0 { + continue + } + t0 := log.Topics[0] + if t0 != topicGovFeeCharged && t0 != topicGTokenFeeCharged { + continue + } + rawHex := strings.TrimPrefix(log.Data, "0x") + if len(rawHex) < 64 { + continue + } + rawInt := new(big.Int) + rawInt.SetString(rawHex[:64], 16) + + amtF := new(big.Float).SetInt(rawInt) + amtF.Quo(amtF, divisor) + amtUSD, _ := amtF.Float64() + micro := int64(math.Round(amtUSD * collPriceUSD * 1e6)) + + switch t0 { + case topicGovFeeCharged: + govMicro += micro + case topicGTokenFeeCharged: + lpMicro += micro + } + } + + return govMicro, lpMicro, nil +} diff --git a/harnesses/apps/internal/collect/rest/gmx_v2.go b/harnesses/apps/internal/collect/rest/gmx_v2.go new file mode 100644 index 00000000..8dd82a8d --- /dev/null +++ b/harnesses/apps/internal/collect/rest/gmx_v2.go @@ -0,0 +1,301 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +// GMX v2 Subsquid GraphQL endpoints (first-party, GMX-operated). +// Amounts use GMX's internal PRICE_PRECISION (1e30) — store raw with Decimals=30. +const ( + gmxArbitrumSQ = "https://gmx.squids.live/gmx-synthetics-arbitrum:prod/api/graphql" + gmxAvaxSQ = "https://gmx.squids.live/gmx-synthetics-avalanche:prod/api/graphql" +) + +// Protocol revenue split per GMX governance (as of 2026): +// +// 63% → GM / GLV LPs (beneficiary = "lp") +// 27% → GMX buyback (beneficiary = "burn") +// 10% → Treasury (beneficiary = "treasury") +// +// Note: staking distributions suspended as of Mar 2026; buyback tokens +// accumulate in treasury. We still book accrual at 37% protocol revenue. +const ( + gmxLPBps = 6300 // 63% + gmxBuybackBps = 2700 // 27% + gmxTreasuryBps = 1000 // 10% +) + +type GMXv2Collector struct { + client *http.Client + endpoint string + chainID string +} + +func NewGMXv2Arbitrum() *GMXv2Collector { + return &GMXv2Collector{ + client: &http.Client{Timeout: 30 * time.Second}, + endpoint: gmxArbitrumSQ, + chainID: "arbitrum", + } +} + +func NewGMXv2Avalanche() *GMXv2Collector { + return &GMXv2Collector{ + client: &http.Client{Timeout: 30 * time.Second}, + endpoint: gmxAvaxSQ, + chainID: "avalanche", + } +} + +func (c *GMXv2Collector) Name() string { + return fmt.Sprintf("gmx-v2-%s-subsquid", c.chainID) +} + +type gmxDayFees struct { + Ts int64 + PosGross string // totalPositionFeeUsd (BigInt, 1e30) + PosForPool string // totalPositionFeeUsdForPool (BigInt, 1e30) + SwapRecv string // totalFeeReceiverUsd from swapFees (BigInt, 1e30) + SwapForPool string // totalFeeUsdForPool from swapFees (BigInt, 1e30) + BorrowPool string // totalBorrowingFeeUsd → all to pool (BigInt, 1e30) +} + +func (c *GMXv2Collector) Collect( + ctx context.Context, + deploymentID string, + from, to spec.Cursor, + out chan<- spec.FeeEvent, +) (spec.Cursor, error) { + cursor := from + + // Fetch last 90 days of daily data from Subsquid. + // cursor.Height is the unix timestamp (seconds) of the last processed day. + tsFrom := int(from.Height) + if tsFrom == 0 { + // Default: start 90 days ago. + tsFrom = int(time.Now().AddDate(0, 0, -90).Truncate(24 * time.Hour).Unix()) + } + tsTo := int(to.Ts.Truncate(24 * time.Hour).Unix()) + + days, err := c.fetchDays(ctx, tsFrom, tsTo) + if err != nil { + return cursor, fmt.Errorf("gmx-v2: %w", err) + } + + for _, d := range days { + if uint64(d.Ts) <= from.Height { + continue + } + ts := time.Unix(d.Ts, 0).UTC() + + // Skip current incomplete day. + if ts.Truncate(24 * time.Hour).Equal(time.Now().UTC().Truncate(24 * time.Hour)) { + continue + } + + emitBigInt := func(component, beneficiary, amountRaw string) { + if amountRaw == "" || amountRaw == "0" { + return + } + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("gmx-v2:%s:%d:%s:%s", c.chainID, d.Ts, component, beneficiary), + Ts: ts, + Height: uint64(d.Ts), + Component: component, + Beneficiary: beneficiary, + Token: "USD", + AmountRaw: amountRaw, + Decimals: 30, + Market: "all", + Finality: spec.FinalityFinal, + Source: c.Name(), + } + } + + // Position fees: gross = PosGross, split between LP (posForPool) and protocol (posGross - posForPool). + // Compute posForProtocol = posGross - posForPool in string form for BigInt safety. + posProtocol := bigSubStr(d.PosGross, d.PosForPool) + emitBigInt("position_fee", "lp", d.PosForPool) + emitBigInt("position_fee", "burn", posProtocol) + + // Swap fees: receiver is protocol's cut, pool is LP's cut. + emitBigInt("swap_fee", "burn", d.SwapRecv) + emitBigInt("swap_fee", "lp", d.SwapForPool) + + // Borrowing fees go entirely to the pool (LPs), not to protocol. + emitBigInt("borrow_fee", "lp", d.BorrowPool) + + cursor = spec.Cursor{Height: uint64(d.Ts), Ts: ts, Finalized: true} + } + + return cursor, nil +} + +// fetchDays queries both positionFees and swapFees daily data and merges by timestamp. +func (c *GMXv2Collector) fetchDays(ctx context.Context, tsFrom, tsTo int) ([]gmxDayFees, error) { + // Fetch position fees. + posQuery := fmt.Sprintf(`{ + positionFeesInfoWithPeriods(orderBy:timestamp_ASC, where:{period_eq:"1d",timestamp_gte:%d,timestamp_lt:%d}, limit:500) { + timestamp totalPositionFeeUsd totalPositionFeeUsdForPool totalBorrowingFeeUsd + } + }`, tsFrom, tsTo) + + type posEntry struct { + Timestamp int `json:"timestamp"` + TotalPositionFeeUsd string `json:"totalPositionFeeUsd"` + TotalPositionFeeUsdForPool string `json:"totalPositionFeeUsdForPool"` + TotalBorrowingFeeUsd string `json:"totalBorrowingFeeUsd"` + } + var posResp struct { + Data struct { + Items []posEntry `json:"positionFeesInfoWithPeriods"` + } `json:"data"` + Errors []struct{ Message string } `json:"errors"` + } + if err := c.gqlQuery(ctx, posQuery, &posResp); err != nil { + return nil, fmt.Errorf("position fees: %w", err) + } + if len(posResp.Errors) > 0 { + return nil, fmt.Errorf("position fees gql: %s", posResp.Errors[0].Message) + } + + // Fetch swap fees. + swapQuery := fmt.Sprintf(`{ + swapFeesInfoWithPeriods(orderBy:timestamp_ASC, where:{period_eq:"1d",timestamp_gte:%d,timestamp_lt:%d}, limit:500) { + timestamp totalFeeReceiverUsd totalFeeUsdForPool + } + }`, tsFrom, tsTo) + + type swapEntry struct { + Timestamp int `json:"timestamp"` + TotalFeeReceiverUsd string `json:"totalFeeReceiverUsd"` + TotalFeeUsdForPool string `json:"totalFeeUsdForPool"` + } + var swapResp struct { + Data struct { + Items []swapEntry `json:"swapFeesInfoWithPeriods"` + } `json:"data"` + Errors []struct{ Message string } `json:"errors"` + } + if err := c.gqlQuery(ctx, swapQuery, &swapResp); err != nil { + return nil, fmt.Errorf("swap fees: %w", err) + } + if len(swapResp.Errors) > 0 { + return nil, fmt.Errorf("swap fees gql: %s", swapResp.Errors[0].Message) + } + + // Merge by timestamp. + posByTs := map[int]posEntry{} + for _, p := range posResp.Data.Items { + posByTs[p.Timestamp] = p + } + swapByTs := map[int]swapEntry{} + for _, s := range swapResp.Data.Items { + swapByTs[s.Timestamp] = s + } + + // Collect all unique timestamps. + seen := map[int]bool{} + for ts := range posByTs { + seen[ts] = true + } + for ts := range swapByTs { + seen[ts] = true + } + + var out []gmxDayFees + for ts := range seen { + p := posByTs[ts] + s := swapByTs[ts] + out = append(out, gmxDayFees{ + Ts: int64(ts), + PosGross: orZero(p.TotalPositionFeeUsd), + PosForPool: orZero(p.TotalPositionFeeUsdForPool), + SwapRecv: orZero(s.TotalFeeReceiverUsd), + SwapForPool: orZero(s.TotalFeeUsdForPool), + BorrowPool: orZero(p.TotalBorrowingFeeUsd), + }) + } + + // Sort by timestamp ascending. + for i := 1; i < len(out); i++ { + for j := i; j > 0 && out[j].Ts < out[j-1].Ts; j-- { + out[j], out[j-1] = out[j-1], out[j] + } + } + return out, nil +} + +func (c *GMXv2Collector) gqlQuery(ctx context.Context, query string, dest interface{}) error { + body, _ := json.Marshal(map[string]string{"query": query}) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "ocb-apps/1.0") + + resp, err := c.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP %d", resp.StatusCode) + } + return json.NewDecoder(resp.Body).Decode(dest) +} + +// bigSubStr subtracts two base-10 integer strings: a - b. +// Uses strconv for small values; falls back to manual subtraction for large BigInt strings. +func bigSubStr(a, b string) string { + // Simple approach: parse both, subtract. + // For 30-decimal GMX values, a and b are ~35 digits — use Go's math/big indirectly + // by doing string arithmetic. Since b <= a always (pool ≤ gross), result is non-negative. + if a == "" || a == "0" { + return "0" + } + if b == "" || b == "0" { + return a + } + // Pad to same length. + for len(a) < len(b) { + a = "0" + a + } + for len(b) < len(a) { + b = "0" + b + } + result := make([]byte, len(a)) + borrow := 0 + for i := len(a) - 1; i >= 0; i-- { + diff := int(a[i]-'0') - int(b[i]-'0') - borrow + if diff < 0 { + diff += 10 + borrow = 1 + } else { + borrow = 0 + } + result[i] = byte('0' + diff) + } + // Trim leading zeros. + s := string(result) + for len(s) > 1 && s[0] == '0' { + s = s[1:] + } + return s +} + +func orZero(s string) string { + if s == "" { + return "0" + } + return s +} diff --git a/harnesses/apps/internal/collect/rest/hyperliquid.go b/harnesses/apps/internal/collect/rest/hyperliquid.go new file mode 100644 index 00000000..dd4d35d3 --- /dev/null +++ b/harnesses/apps/internal/collect/rest/hyperliquid.go @@ -0,0 +1,144 @@ +package rest + +import ( + "context" + "encoding/json" + "fmt" + "math" + "net/http" + "time" + + "github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec" +) + +// fillsAggURL is the hl-fills-agg service running on the SGP node alongside +// the Hyperliquid non-validating node. It reads hourly node_fills_by_block +// files and returns daily fee totals (gross, builder, net) in USDC. +const fillsAggURL = "http://15.235.224.14:2116" + +// fillsEpoch is the earliest date available in the node fills archive. +const fillsEpoch = "20260601" + +type HyperliquidCollector struct { + client *http.Client +} + +func NewHyperliquid() *HyperliquidCollector { + return &HyperliquidCollector{client: &http.Client{Timeout: 60 * time.Second}} +} + +func (c *HyperliquidCollector) Name() string { return "hyperliquid-node-fills" } + +type hlDailySummary struct { + Date string `json:"date"` // "YYYYMMDD" + GrossUSDC float64 `json:"gross_usdc"` // all fees paid (taker + maker) + BuilderUSDC float64 `json:"builder_usdc"` // third-party builder codes cut + NetUSDC float64 `json:"net_usdc"` // gross - builder → goes to AF + HLP + deployers + Fills int64 `json:"fills"` + Hours int `json:"hours"` +} + +func (c *HyperliquidCollector) Collect( + ctx context.Context, + deploymentID string, + from, to spec.Cursor, + out chan<- spec.FeeEvent, +) (spec.Cursor, error) { + cursor := from + + // Determine the start date string. + // cursor.Height encodes the unix timestamp (seconds) of the last processed + // day's midnight UTC. Zero means start from the fills epoch. + var startDate string + if from.Height == 0 { + startDate = fillsEpoch + } else { + startDate = time.Unix(int64(from.Height), 0).UTC().Format("20060102") + } + endDate := to.Ts.UTC().Format("20060102") + + days, err := c.fetchDailySummaries(ctx, startDate, endDate) + if err != nil { + return cursor, fmt.Errorf("hyperliquid: fills-agg: %w", err) + } + + for _, d := range days { + ts, err := time.Parse("20060102", d.Date) + if err != nil { + continue + } + ts = ts.UTC() + + h := uint64(ts.Unix()) + if h < from.Height { + continue + } + + // Skip partial days (< 23 hours) — they're incomplete. + // Today's day will be partial and should not be persisted. + if d.Hours < 23 { + continue + } + + if d.NetUSDC <= 0 { + continue + } + + // net_usdc = all fills fees minus builder codes. + // This is the total amount that flows to AF + HLP + deployers. + // We classify as taker_fee/burn since the vast majority (~97-99%) + // goes to the Assistance Fund (which burns HYPE), per the HL docs. + // The HLP vault share and deployer share are not separable per-fill + // and are small relative to the total. + amountMicro := int64(math.Round(d.NetUSDC * 1e6)) + out <- spec.FeeEvent{ + DeploymentID: deploymentID, + EventKey: fmt.Sprintf("hl:node-fills:%s:net", d.Date), + Ts: ts, + Height: h, + Component: "taker_fee", + Beneficiary: "burn", + Token: "USDC", + AmountRaw: fmt.Sprintf("%d", amountMicro), + Decimals: 6, + Market: "all", + Finality: spec.FinalityFinal, + Source: "hyperliquid-node-fills", + Meta: map[string]string{ + "gross_usdc": fmt.Sprintf("%.2f", d.GrossUSDC), + "builder_usdc": fmt.Sprintf("%.2f", d.BuilderUSDC), + "fills": fmt.Sprintf("%d", d.Fills), + "hours": fmt.Sprintf("%d", d.Hours), + }, + } + + cursor = spec.Cursor{Height: h + 86400, Ts: ts, Finalized: true} + } + + return cursor, nil +} + +func (c *HyperliquidCollector) fetchDailySummaries(ctx context.Context, from, to string) ([]hlDailySummary, error) { + url := fmt.Sprintf("%s/daily?from=%s&to=%s", fillsAggURL, from, to) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, 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, url) + } + + var result []hlDailySummary + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("decode: %w", err) + } + return result, nil +} 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. diff --git a/harnesses/apps/scripts/hl-fills-agg.py b/harnesses/apps/scripts/hl-fills-agg.py new file mode 100644 index 00000000..009cd412 --- /dev/null +++ b/harnesses/apps/scripts/hl-fills-agg.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +""" +hl-fills-agg: HTTP server exposing daily Hyperliquid fee aggregates. +Reads node fills from /home/ubuntu/hl/data/node_fills_by_block/hourly/{YYYYMMDD}/{H}. +Caches computed days to /home/ubuntu/hl-agg/cache/{YYYYMMDD}.json. + +GET /daily?from=20260601&to=20260809 +-> [{"date":"20260601","gross_usdc":"...", "builder_usdc":"...","net_usdc":"...","fills":N,"hours":24}, ...] +""" + +import json +import os +import sys +import threading +import time +from datetime import date, timedelta +from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.parse import urlparse, parse_qs + +FILLS_DIR = "/home/ubuntu/hl/data/node_fills_by_block/hourly" +CACHE_DIR = "/home/ubuntu/hl-agg/cache" +PORT = 2116 + +os.makedirs(CACHE_DIR, exist_ok=True) + +_lock = threading.Lock() + + +def aggregate_day(date_str: str, is_today: bool) -> dict | None: + cache_file = os.path.join(CACHE_DIR, f"{date_str}.json") + if not is_today and os.path.exists(cache_file): + with open(cache_file) as f: + return json.load(f) + + day_dir = os.path.join(FILLS_DIR, date_str) + if not os.path.isdir(day_dir): + return None + + hours = sorted(os.listdir(day_dir)) + gross = 0.0 + builder = 0.0 + fills = 0 + + for h in hours: + path = os.path.join(day_dir, h) + try: + with open(path) as f: + for line in f: + try: + d = json.loads(line) + for item in d.get("events") or []: + fill = item[1] + gross += float(fill.get("fee", 0) or 0) + builder += float(fill.get("builderFee", 0) or 0) + fills += 1 + except Exception: + pass + except OSError: + pass + + result = { + "date": date_str, + "gross_usdc": round(gross, 6), + "builder_usdc": round(builder, 6), + "net_usdc": round(gross - builder, 6), + "fills": fills, + "hours": len(hours), + } + + if not is_today: + tmp = cache_file + ".tmp" + with open(tmp, "w") as f: + json.dump(result, f) + os.replace(tmp, cache_file) + + return result + + +def backfill_worker(): + """Pre-aggregate all past days not yet cached.""" + time.sleep(5) + today = date.today() + d = date(2026, 6, 1) + while d < today: + ds = d.strftime("%Y%m%d") + cache_file = os.path.join(CACHE_DIR, f"{ds}.json") + if not os.path.exists(cache_file): + print(f"backfill: aggregating {ds}", flush=True) + aggregate_day(ds, False) + d += timedelta(days=1) + print("backfill: done", flush=True) + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + parsed = urlparse(self.path) + if parsed.path == "/health": + self._json({"status": "ok"}) + return + if parsed.path != "/daily": + self.send_response(404) + self.end_headers() + return + + qs = parse_qs(parsed.query) + from_str = qs.get("from", ["20260601"])[0] + to_str = qs.get("to", [date.today().strftime("%Y%m%d")])[0] + + try: + from_d = date(int(from_str[:4]), int(from_str[4:6]), int(from_str[6:8])) + to_d = date(int(to_str[:4]), int(to_str[4:6]), int(to_str[6:8])) + except (ValueError, IndexError): + self.send_response(400) + self.end_headers() + return + + today = date.today() + # Only return cached days (or today on-demand). + # Uncached past days are skipped — the backfill thread will cache them. + # This keeps the HTTP response fast regardless of how many uncached days exist. + results = [] + d = from_d + while d <= to_d: + ds = d.strftime("%Y%m%d") + is_today = d == today + cache_file = os.path.join(CACHE_DIR, f"{ds}.json") + if is_today: + r = aggregate_day(ds, True) + elif os.path.exists(cache_file): + with open(cache_file) as f: + r = json.load(f) + else: + d += timedelta(days=1) + continue + if r: + results.append(r) + d += timedelta(days=1) + + self._json(results) + + def _json(self, data): + body = json.dumps(data).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, fmt, *args): + pass + + +if __name__ == "__main__": + threading.Thread(target=backfill_worker, daemon=True).start() + print(f"hl-fills-agg listening on :{PORT}", flush=True) + HTTPServer(("0.0.0.0", PORT), Handler).serve_forever() diff --git a/harnesses/metadata-coverage/cmd/script/defined_auth.go b/harnesses/metadata-coverage/cmd/script/defined_auth.go index 716f05f0..717e75ad 100644 --- a/harnesses/metadata-coverage/cmd/script/defined_auth.go +++ b/harnesses/metadata-coverage/cmd/script/defined_auth.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "os" "strings" "sync" "time" @@ -57,8 +58,12 @@ func decodeJWTExpiration(token string) (time.Time, error) { return time.Unix(claims.Exp, 0), nil } -// GetDefinedJWTToken returns a cached JWT token or generates a new one if expired +// GetDefinedJWTToken returns a cached JWT token or generates a new one if expired. +// If CODEX_JWT env var is set, it is returned directly (bypasses session-cookie flow). func GetDefinedJWTToken(sessionCookie string) (string, error) { + if jwt := os.Getenv("CODEX_JWT"); jwt != "" { + return jwt, nil + } globalTokenCache.mu.RLock() // Check if we have a valid cached token @@ -144,7 +149,7 @@ func generateDefinedJWTToken(sessionCookie string) (string, error) { req.Header.Set("sec-fetch-dest", "empty") req.Header.Set("sec-fetch-mode", "cors") req.Header.Set("sec-fetch-site", "same-origin") - req.AddCookie(&http.Cookie{Name: "session", Value: sessionCookie}) + req.AddCookie(&http.Cookie{Name: "defined-attestation-token", Value: sessionCookie}) fmt.Println("[DEFINED-AUTH] Sending POST request to https://www.defined.fi/api...") resp, err := client.Do(req) diff --git a/harnesses/metadata-coverage/cmd/script/scrape_session.go b/harnesses/metadata-coverage/cmd/script/scrape_session.go index 4607912c..26d3f770 100644 --- a/harnesses/metadata-coverage/cmd/script/scrape_session.go +++ b/harnesses/metadata-coverage/cmd/script/scrape_session.go @@ -42,7 +42,7 @@ func ScrapeDefinedSessionCookie() (string, error) { } for _, cookie := range cookieParams { - if cookie.Name == "session" { + if cookie.Name == "defined-attestation-token" { sessionCookie = cookie.Value return nil } diff --git a/harnesses/network-coverage/cmd/script/codex.go b/harnesses/network-coverage/cmd/script/codex.go index db31dde1..60db0b81 100644 --- a/harnesses/network-coverage/cmd/script/codex.go +++ b/harnesses/network-coverage/cmd/script/codex.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "os" "strconv" "time" ) @@ -99,7 +100,11 @@ func fetchCodex(cfg *Config) ProviderResult { // getCodexJWT mints a Defined.fi JWT from the session cookie, or pulls a // pre-minted one from a sidecar URL if DEFINED_TOKEN_SERVICE_URL is set. +// If CODEX_JWT env var is set, it is returned directly (bypasses all other flows). func getCodexJWT(cfg *Config) (string, error) { + if jwt := os.Getenv("CODEX_JWT"); jwt != "" { + return jwt, nil + } // If a sidecar token service is configured, try it first. On any error, // silently fall back to inline mint — the sidecar isn't authoritative. if cfg.DefinedTokenURL != "" { @@ -127,7 +132,7 @@ func getCodexJWT(cfg *Config) (string, error) { req.Header.Set("Accept", "application/json") req.Header.Set("Origin", "https://www.defined.fi") req.Header.Set("Referer", "https://www.defined.fi/") - req.Header.Set("Cookie", "session-token="+cfg.CodexSessionCookie) + req.AddCookie(&http.Cookie{Name: "defined-attestation-token", Value: cfg.CodexSessionCookie}) resp, err := client.Do(req) if err != nil { diff --git a/harnesses/pm-rate-limits/cmd/script/codex_auth.go b/harnesses/pm-rate-limits/cmd/script/codex_auth.go index b06ed1eb..a4e1fd39 100644 --- a/harnesses/pm-rate-limits/cmd/script/codex_auth.go +++ b/harnesses/pm-rate-limits/cmd/script/codex_auth.go @@ -14,6 +14,7 @@ import ( "io" "log" "net/http" + "os" "strings" "sync" "time" @@ -59,7 +60,11 @@ func decodeJWTExpiration(token string) (time.Time, error) { // GetCodexJWT returns a cached short-lived JWT, minting a new one when the // cached one is within 1h of expiry. +// If CODEX_JWT env var is set, it is returned directly (bypasses session-cookie flow). func GetCodexJWT(sessionCookie string) (string, error) { + if jwt := os.Getenv("CODEX_JWT"); jwt != "" { + return jwt, nil + } codexTokenCache.mu.RLock() if codexTokenCache.token != "" && time.Now().Before(codexTokenCache.expiresAt.Add(-1*time.Hour)) { t := codexTokenCache.token @@ -120,7 +125,7 @@ func mintCodexJWT(sessionCookie string) (string, error) { req.Header.Set("sec-fetch-dest", "empty") req.Header.Set("sec-fetch-mode", "cors") req.Header.Set("sec-fetch-site", "same-origin") - req.AddCookie(&http.Cookie{Name: "session", Value: sessionCookie}) + req.AddCookie(&http.Cookie{Name: "defined-attestation-token", Value: sessionCookie}) resp, err := client.Do(req) if err != nil { diff --git a/harnesses/rpc-capabilities/cmd/script/config.go b/harnesses/rpc-capabilities/cmd/script/config.go index ab02e508..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")}, }, @@ -1339,19 +1322,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,37 +1341,20 @@ 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. - { - 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). 5 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")}, - {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 +1362,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 +1381,33 @@ 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). 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: "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 +1420,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). 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")}, - {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 +1439,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 +1456,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")}, }, }, diff --git a/harnesses/solana-exec/Dockerfile b/harnesses/solana-exec/Dockerfile new file mode 100644 index 00000000..a3752b65 --- /dev/null +++ b/harnesses/solana-exec/Dockerfile @@ -0,0 +1,15 @@ +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 +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/solana-exec/cmd/api/main.go b/harnesses/solana-exec/cmd/api/main.go new file mode 100644 index 00000000..3fcf7cac --- /dev/null +++ b/harnesses/solana-exec/cmd/api/main.go @@ -0,0 +1,202 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "net/http" + "os" + "sort" + "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("exec-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/exec-leaderboard", corsJSON(handleExecLeaderboard(pool))) + + addr := ":2116" + log.Printf("exec-api listening on %s", addr) + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatalf("exec-api: serve: %v", err) + } +} + +// WindowStats holds aggregated execution metrics for one time window. +type WindowStats struct { + TxCount int64 `json:"txCount"` + AvgPriorityFeeLamports float64 `json:"avgPriorityFeeLamports"` + P50CUPriceMicro float64 `json:"p50CUPriceMicro"` + P95CUPriceMicro float64 `json:"p95CUPriceMicro"` + AvgPlatformFeeLamports float64 `json:"avgPlatformFeeLamports"` + JitoRate float64 `json:"jitoRate"` + AvgCUConsumed float64 `json:"avgCUConsumed"` +} + +type PlatformRow struct { + Platform string `json:"platform"` + LatestBkt string `json:"latestBucket"` + Windows map[string]WindowStats `json:"windows"` +} + +type ExecLeaderboardResponse struct { + UpdatedAt string `json:"updatedAt"` + Platforms []PlatformRow `json:"platforms"` +} + +func handleExecLeaderboard(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 + platform, + -- weighted averages: SUM(avg×count)/SUM(count) avoids skewing by small off-peak buckets + SUM(avg_priority_fee_lamports * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') + / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_prio, + AVG(p50_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_p50, + AVG(p95_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_p95, + SUM(avg_platform_fee_lamports * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') + / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_pfee, + SUM(jito_rate * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') + / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_jito, + SUM(avg_cu_consumed * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') + / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_cu, + SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_count, + + SUM(avg_priority_fee_lamports * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') + / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_prio, + AVG(p50_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_p50, + AVG(p95_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_p95, + SUM(avg_platform_fee_lamports * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') + / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_pfee, + SUM(jito_rate * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') + / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_jito, + SUM(avg_cu_consumed * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') + / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_cu, + SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_count, + + SUM(avg_priority_fee_lamports * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') + / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_prio, + AVG(p50_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_p50, + AVG(p95_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_p95, + SUM(avg_platform_fee_lamports * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') + / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_pfee, + SUM(jito_rate * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') + / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_jito, + SUM(avg_cu_consumed * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') + / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_cu, + SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_count, + + MAX(bucket_start)::text AS latest_bucket + FROM solana_exec_facts + GROUP BY platform`, + ) + if err != nil { + log.Printf("exec-api: query: %v", err) + http.Error(w, "internal", http.StatusInternalServerError) + return + } + defer rows.Close() + + var platforms []PlatformRow + for rows.Next() { + var plt, latestBkt string + var h24Prio, h24P50, h24P95, h24Pfee, h24Jito, h24Cu *float64 + var h24Count *int64 + var d7Prio, d7P50, d7P95, d7Pfee, d7Jito, d7Cu *float64 + var d7Count *int64 + var d30Prio, d30P50, d30P95, d30Pfee, d30Jito, d30Cu *float64 + var d30Count *int64 + + if err := rows.Scan( + &plt, + &h24Prio, &h24P50, &h24P95, &h24Pfee, &h24Jito, &h24Cu, &h24Count, + &d7Prio, &d7P50, &d7P95, &d7Pfee, &d7Jito, &d7Cu, &d7Count, + &d30Prio, &d30P50, &d30P95, &d30Pfee, &d30Jito, &d30Cu, &d30Count, + &latestBkt, + ); err != nil { + log.Printf("exec-api: scan: %v", err) + continue + } + + p := PlatformRow{ + Platform: plt, + LatestBkt: latestBkt, + Windows: map[string]WindowStats{ + "24h": buildWindow(h24Prio, h24P50, h24P95, h24Pfee, h24Jito, h24Cu, h24Count), + "7d": buildWindow(d7Prio, d7P50, d7P95, d7Pfee, d7Jito, d7Cu, d7Count), + "30d": buildWindow(d30Prio, d30P50, d30P95, d30Pfee, d30Jito, d30Cu, d30Count), + }, + } + platforms = append(platforms, p) + } + + // Sort by 30d tx count descending. + sort.Slice(platforms, func(i, j int) bool { + return platforms[i].Windows["30d"].TxCount > platforms[j].Windows["30d"].TxCount + }) + + resp := ExecLeaderboardResponse{ + UpdatedAt: time.Now().UTC().Format(time.RFC3339), + Platforms: platforms, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + } +} + +func buildWindow(prio, p50, p95, pfee, jito, cu *float64, count *int64) WindowStats { + ws := WindowStats{} + if count != nil { + ws.TxCount = *count + } + if prio != nil { + ws.AvgPriorityFeeLamports = *prio + } + if p50 != nil { + ws.P50CUPriceMicro = *p50 + } + if p95 != nil { + ws.P95CUPriceMicro = *p95 + } + if pfee != nil { + ws.AvgPlatformFeeLamports = *pfee + } + if jito != nil { + ws.JitoRate = *jito + } + if cu != nil { + ws.AvgCUConsumed = *cu + } + return ws +} + +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/harnesses/solana-exec/cmd/collector/main.go b/harnesses/solana-exec/cmd/collector/main.go new file mode 100644 index 00000000..909c7774 --- /dev/null +++ b/harnesses/solana-exec/cmd/collector/main.go @@ -0,0 +1,181 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/ChainBench/OpenChainBench/harnesses/solana-exec/internal/helius" + "github.com/ChainBench/OpenChainBench/harnesses/solana-exec/internal/platform" + "github.com/ChainBench/OpenChainBench/harnesses/solana-exec/internal/store" +) + +// baseFeePerSig is the fixed Solana base fee per signature in lamports. +const baseFeePerSig = 5000 + +func main() { + ctx := context.Background() + + db, err := store.New(ctx, mustEnv("DATABASE_URL")) + if err != nil { + log.Fatalf("collector: %v", err) + } + defer db.Close() + + h := helius.New(mustEnv("HELIUS_API_KEY")) + + log.Printf("collector: monitoring %d platforms", len(platform.FeeAccounts)) + + for { + for plt, feeAccount := range platform.FeeAccounts { + if err := collect(ctx, db, h, plt, feeAccount); err != nil { + log.Printf("collector: %s: %v", plt, err) + } + } + time.Sleep(10 * time.Minute) + } +} + +func collect(ctx context.Context, db *store.DB, h *helius.Client, plt, feeAccount string) error { + cursor, err := db.GetCursor(ctx, plt) + if err != nil { + return fmt.Errorf("get cursor: %w", err) + } + + const sigLimit = 100 + // Paginate through ALL signatures newer than cursor (newest-first per page). + // Each page uses `before=oldestSigInPreviousPage` to walk backwards until + // we exhaust the window. This guarantees complete coverage regardless of volume. + var sigs []helius.SigEntry + before := "" + for { + batch, err := h.GetSignaturesForAddress(ctx, feeAccount, sigLimit, cursor.LastSig, before) + if err != nil { + return fmt.Errorf("get sigs (before=%s): %w", before, err) + } + sigs = append(sigs, batch...) + if len(batch) < sigLimit { + break // last page + } + before = batch[len(batch)-1].Signature + time.Sleep(300 * time.Millisecond) // respect Helius free-tier rate limit between pages + } + if len(sigs) == 0 { + return nil + } + if len(sigs) > sigLimit { + log.Printf("collector: %s: paginated %d raw sigs (%d pages)", plt, len(sigs), (len(sigs)+sigLimit-1)/sigLimit) + } + + // Reverse to process oldest-first so cursor is always the true watermark. + reversed := make([]helius.SigEntry, len(sigs)) + for i, s := range sigs { + reversed[len(sigs)-1-i] = s + } + + // Pre-filter failed txs to avoid wasting enhanced-API credits. + // Failed txs don't generate platform fees; priority-fee stats should reflect + // the cost of successful trades only. + var sigStrs []string + for _, s := range reversed { + if s.Err == nil { + sigStrs = append(sigStrs, s.Signature) + } + } + + // Count successful sigs per hour bucket from raw pagination (full volume, no enhanced API cost). + hourBuckets := make(map[time.Time]int64) + for _, s := range reversed { + if s.Err != nil || s.BlockTime == 0 { + continue + } + bucket := time.Unix(s.BlockTime, 0).UTC().Truncate(time.Hour) + hourBuckets[bucket]++ + } + if err := db.UpsertRawCounts(ctx, plt, hourBuckets); err != nil { + return fmt.Errorf("upsert raw counts: %w", err) + } + + if len(sigStrs) == 0 { + // All sigs in this batch were failed txs; advance cursor and skip. + newest := sigs[0] + return db.SaveCursor(ctx, plt, newest.Signature, newest.Slot) + } + + // Cap enhanced API at 100 sigs per poll (Helius free-tier budget: ~432K CUs/month). + // Fee quality metrics are sampled; tx_count comes from raw counts above. + if len(sigStrs) > 100 { + sigStrs = sigStrs[:100] + } + + txs, err := h.GetEnhancedTransactions(ctx, sigStrs) + if err != nil { + return fmt.Errorf("get enhanced txs: %w", err) + } + + events := make([]store.ExecEvent, 0, len(txs)) + for _, tx := range txs { + if tx.TransactionError != nil || tx.Timestamp == 0 { + continue + } + + priorityFee := tx.Fee - baseFeePerSig + if priorityFee < 0 { + priorityFee = 0 + } + + var platformFeeLamports, jitoTipLamports int64 + isJito := false + for _, xfer := range tx.NativeTransfers { + if xfer.ToUserAccount == feeAccount { + platformFeeLamports += xfer.Amount + } + if platform.JitoTipAccounts[xfer.ToUserAccount] { + jitoTipLamports += xfer.Amount + isJito = true + } + } + + var cuPriceMicro int64 + if tx.ComputeUnitsConsumed > 0 && priorityFee > 0 { + cuPriceMicro = priorityFee * 1_000_000 / tx.ComputeUnitsConsumed + } + + events = append(events, store.ExecEvent{ + Sig: tx.Signature, + Platform: plt, + Slot: tx.Slot, + BlockTime: time.Unix(tx.Timestamp, 0).UTC(), + TotalFeeLamports: tx.Fee, + PriorityFeeLamports: priorityFee, + PlatformFeeLamports: platformFeeLamports, + IsJitoBundle: isJito, + JitoTipLamports: jitoTipLamports, + CUConsumed: tx.ComputeUnitsConsumed, + CUPriceMicro: cuPriceMicro, + }) + } + + if err := db.UpsertEvents(ctx, events); err != nil { + return fmt.Errorf("upsert: %w", err) + } + + // Advance cursor to the newest sig (first in original order = last in reversed). + newest := sigs[0] + if err := db.SaveCursor(ctx, plt, newest.Signature, newest.Slot); err != nil { + return fmt.Errorf("save cursor: %w", err) + } + + log.Printf("collector: %s: ingested %d txs (newest slot %d)", plt, len(events), newest.Slot) + return nil +} + +func mustEnv(key string) string { + v := os.Getenv(key) + if v == "" { + log.Fatalf("missing env: %s", key) + } + return v +} diff --git a/harnesses/solana-exec/cmd/materializer/main.go b/harnesses/solana-exec/cmd/materializer/main.go new file mode 100644 index 00000000..98fc753a --- /dev/null +++ b/harnesses/solana-exec/cmd/materializer/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "log" + "os" + "time" + + "github.com/ChainBench/OpenChainBench/harnesses/solana-exec/internal/platform" + "github.com/ChainBench/OpenChainBench/harnesses/solana-exec/internal/store" +) + +func main() { + ctx := context.Background() + + db, err := store.New(ctx, mustEnv("DATABASE_URL")) + if err != nil { + log.Fatalf("materializer: %v", err) + } + defer db.Close() + + for { + for plt := range platform.FeeAccounts { + if err := db.Materialize(ctx, plt); err != nil { + log.Printf("materializer: %s: %v", plt, err) + } else { + log.Printf("materializer: %s: done", plt) + } + } + time.Sleep(5 * time.Minute) + } +} + +func mustEnv(key string) string { + v := os.Getenv(key) + if v == "" { + log.Fatalf("missing env: %s", key) + } + return v +} diff --git a/harnesses/solana-exec/docker-compose.solana-exec.yml b/harnesses/solana-exec/docker-compose.solana-exec.yml new file mode 100644 index 00000000..63d252d0 --- /dev/null +++ b/harnesses/solana-exec/docker-compose.solana-exec.yml @@ -0,0 +1,46 @@ +# Fragment to merge into /opt/ocb/docker-compose.yml on VPS. +# Requires: ocb-postgres (from docker-compose.apps.yml), HELIUS_API_KEY in env. + +services: + solana-exec-collector: + build: /opt/ocb/harnesses/solana-exec + container_name: ocb-solana-exec-collector + restart: unless-stopped + env_file: /run/ocb/.env.solana-exec + command: ["/app/collector"] + networks: [web] + depends_on: + postgres: + condition: service_healthy + logging: + driver: json-file + options: { max-size: "20m", max-file: "5" } + + solana-exec-materializer: + build: /opt/ocb/harnesses/solana-exec + container_name: ocb-solana-exec-materializer + restart: unless-stopped + env_file: /run/ocb/.env.solana-exec + command: ["/app/materializer"] + networks: [web] + depends_on: + postgres: + condition: service_healthy + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } + + solana-exec-api: + build: /opt/ocb/harnesses/solana-exec + container_name: ocb-solana-exec-api + restart: unless-stopped + env_file: /run/ocb/.env.solana-exec + command: ["/app/api"] + networks: [web] + expose: ["2116"] + depends_on: + postgres: + condition: service_healthy + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } diff --git a/harnesses/solana-exec/env.solana-exec.example b/harnesses/solana-exec/env.solana-exec.example new file mode 100644 index 00000000..d341a264 --- /dev/null +++ b/harnesses/solana-exec/env.solana-exec.example @@ -0,0 +1,10 @@ +# /run/ocb/.env.solana-exec — encrypt with SOPS before deploy +# Shares the same Postgres instance as apps harness. + +POSTGRES_USER=ocb +POSTGRES_PASSWORD=CHANGEME +POSTGRES_DB=apps +DATABASE_URL=postgresql://ocb:CHANGEME@ocb-postgres:5432/apps?sslmode=disable + +# Helius API key — https://helius.dev (free tier works for start) +HELIUS_API_KEY=CHANGEME diff --git a/harnesses/solana-exec/go.mod b/harnesses/solana-exec/go.mod new file mode 100644 index 00000000..868e901f --- /dev/null +++ b/harnesses/solana-exec/go.mod @@ -0,0 +1,14 @@ +module github.com/ChainBench/OpenChainBench/harnesses/solana-exec + +go 1.24.0 + +require github.com/jackc/pgx/v5 v5.7.2 + +require ( + 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 + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/text v0.21.0 // indirect +) diff --git a/harnesses/solana-exec/go.sum b/harnesses/solana-exec/go.sum new file mode 100644 index 00000000..731b5dfd --- /dev/null +++ b/harnesses/solana-exec/go.sum @@ -0,0 +1,28 @@ +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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +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/solana-exec/internal/helius/client.go b/harnesses/solana-exec/internal/helius/client.go new file mode 100644 index 00000000..45bc6c17 --- /dev/null +++ b/harnesses/solana-exec/internal/helius/client.go @@ -0,0 +1,146 @@ +package helius + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +// Client wraps the Helius RPC + enhanced-transactions APIs. +type Client struct { + httpClient *http.Client + apiKey string + rpcURL string // https://mainnet.helius-rpc.com/?api-key=KEY + enhURL string // https://api.helius.xyz/v0/transactions?api-key=KEY +} + +func New(apiKey string) *Client { + return &Client{ + httpClient: &http.Client{Timeout: 30 * time.Second}, + apiKey: apiKey, + rpcURL: fmt.Sprintf("https://mainnet.helius-rpc.com/?api-key=%s", apiKey), + enhURL: fmt.Sprintf("https://api.helius.xyz/v0/transactions?api-key=%s", apiKey), + } +} + +// SigEntry is one result from getSignaturesForAddress. +type SigEntry struct { + Signature string `json:"signature"` + Slot uint64 `json:"slot"` + BlockTime int64 `json:"blockTime"` // unix seconds, may be 0 + Err any `json:"err"` // nil = success +} + +// GetSignaturesForAddress fetches up to `limit` finalized signatures for +// `address`, newest-first. `until` is the exclusive upper bound (cursor); +// `before` is the exclusive lower bound used for pagination (pass "" for +// the first page). +func (c *Client) GetSignaturesForAddress(ctx context.Context, address string, limit int, until, before string) ([]SigEntry, error) { + params := map[string]any{ + "limit": limit, + "commitment": "finalized", + } + if until != "" { + params["until"] = until + } + if before != "" { + params["before"] = before + } + + body, _ := json.Marshal(map[string]any{ + "jsonrpc": "2.0", + "id": 1, + "method": "getSignaturesForAddress", + "params": []any{address, params}, + }) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.rpcURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("helius RPC HTTP %d", resp.StatusCode) + } + + var out struct { + Result []SigEntry `json:"result"` + Error *struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error"` + } + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return nil, fmt.Errorf("helius RPC decode: %w", err) + } + if out.Error != nil { + return nil, fmt.Errorf("helius RPC error %d: %s", out.Error.Code, out.Error.Message) + } + return out.Result, nil +} + +// NativeTransfer is a SOL transfer extracted by Helius. +type NativeTransfer struct { + FromUserAccount string `json:"fromUserAccount"` + ToUserAccount string `json:"toUserAccount"` + Amount int64 `json:"amount"` // lamports +} + +// EnhancedTx is the Helius-parsed transaction. +type EnhancedTx struct { + Signature string `json:"signature"` + Slot uint64 `json:"slot"` + Timestamp int64 `json:"timestamp"` // unix seconds + Fee int64 `json:"fee"` // total fee lamports + FeePayer string `json:"feePayer"` + NativeTransfers []NativeTransfer `json:"nativeTransfers"` + TransactionError any `json:"transactionError"` + ComputeUnitsConsumed int64 `json:"computeUnitsConsumed"` +} + +// GetEnhancedTransactions fetches enhanced parsed transactions for up to 100 +// signatures in a single request. Helius caps the batch at 100. +func (c *Client) GetEnhancedTransactions(ctx context.Context, sigs []string) ([]EnhancedTx, error) { + if len(sigs) == 0 { + return nil, nil + } + if len(sigs) > 100 { + return nil, fmt.Errorf("helius: enhanced batch max 100 sigs, got %d", len(sigs)) + } + + body, _ := json.Marshal(map[string]any{ + "transactions": sigs, + }) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.enhURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("helius enhanced HTTP %d", resp.StatusCode) + } + + var txs []EnhancedTx + if err := json.NewDecoder(resp.Body).Decode(&txs); err != nil { + return nil, fmt.Errorf("helius enhanced decode: %w", err) + } + return txs, nil +} diff --git a/harnesses/solana-exec/internal/platform/platforms.go b/harnesses/solana-exec/internal/platform/platforms.go new file mode 100644 index 00000000..16864d1d --- /dev/null +++ b/harnesses/solana-exec/internal/platform/platforms.go @@ -0,0 +1,24 @@ +package platform + +// FeeAccounts maps platform name → its on-chain fee-receiver account. +// These are the accounts that receive platform fees from user trades. +// Adding a new platform = one line here. +var FeeAccounts = map[string]string{ + "pump.fun": "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM", + // TODO: add FOMO, Axiom, GMGN once their fee accounts are confirmed + // Discover by inspecting any known tx from each platform and finding + // the SOL recipient that is not the user, not Jito, not rent. +} + +// JitoTipAccounts is the authoritative set of 8 Jito tip wallets. +// Verified against solana-tx-landing harness wallets.go (2026-08). +var JitoTipAccounts = map[string]bool{ + "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5": true, + "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe": true, + "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY": true, + "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49": true, + "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh": true, + "ADuUkR4vqLUMWXxW9gh6D6L8pivKeVBBjNo7XZQshxw3": true, + "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL": true, + "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT": true, +} diff --git a/harnesses/solana-exec/internal/store/store.go b/harnesses/solana-exec/internal/store/store.go new file mode 100644 index 00000000..1564a23d --- /dev/null +++ b/harnesses/solana-exec/internal/store/store.go @@ -0,0 +1,165 @@ +package store + +import ( + "context" + "fmt" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +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("store: connect: %w", err) + } + return &DB{pool: pool}, nil +} + +func (db *DB) Close() { db.pool.Close() } + +func (db *DB) Pool() *pgxpool.Pool { return db.pool } + +// ExecEvent is a parsed Solana transaction from a monitored platform. +type ExecEvent struct { + Sig string + Platform string + Slot uint64 + BlockTime time.Time + TotalFeeLamports int64 + PriorityFeeLamports int64 + PlatformFeeLamports int64 + IsJitoBundle bool + JitoTipLamports int64 + CUConsumed int64 + CUPriceMicro int64 +} + +// UpsertEvents inserts exec events idempotently; existing sigs are skipped. +func (db *DB) UpsertEvents(ctx context.Context, events []ExecEvent) error { + if len(events) == 0 { + return nil + } + tx, err := db.pool.Begin(ctx) + if err != nil { + return fmt.Errorf("store: begin: %w", err) + } + defer tx.Rollback(ctx) + + for _, e := range events { + _, err := tx.Exec(ctx, ` + INSERT INTO solana_exec_events + (sig, platform, slot, block_time, + total_fee_lamports, priority_fee_lamports, platform_fee_lamports, + is_jito_bundle, jito_tip_lamports, cu_consumed, cu_price_micro) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) + ON CONFLICT (sig) DO NOTHING`, + e.Sig, e.Platform, e.Slot, e.BlockTime, + e.TotalFeeLamports, e.PriorityFeeLamports, e.PlatformFeeLamports, + e.IsJitoBundle, e.JitoTipLamports, e.CUConsumed, e.CUPriceMicro, + ) + if err != nil { + return fmt.Errorf("store: upsert %s: %w", e.Sig, err) + } + } + return tx.Commit(ctx) +} + +// Cursor holds the last processed signature for a platform. +type Cursor struct { + LastSig string + Slot uint64 +} + +func (db *DB) GetCursor(ctx context.Context, platform string) (Cursor, error) { + var c Cursor + err := db.pool.QueryRow(ctx, + `SELECT last_sig, slot FROM solana_exec_cursors WHERE platform = $1`, platform, + ).Scan(&c.LastSig, &c.Slot) + if err != nil { + return Cursor{}, nil // no cursor yet = start from recent + } + return c, nil +} + +func (db *DB) SaveCursor(ctx context.Context, platform, lastSig string, slot uint64) error { + _, err := db.pool.Exec(ctx, ` + INSERT INTO solana_exec_cursors (platform, last_sig, slot, updated_at) + VALUES ($1,$2,$3,now()) + ON CONFLICT (platform) DO UPDATE SET + last_sig=EXCLUDED.last_sig, slot=EXCLUDED.slot, updated_at=now()`, + platform, lastSig, slot, + ) + return err +} + +// UpsertRawCounts stores hourly successful-tx counts from raw sig pagination. +// counts maps bucket_start (hour-truncated UTC) → total successful sigs that hour. +func (db *DB) UpsertRawCounts(ctx context.Context, platform string, counts map[time.Time]int64) error { + if len(counts) == 0 { + return nil + } + tx, err := db.pool.Begin(ctx) + if err != nil { + return fmt.Errorf("store: begin: %w", err) + } + defer tx.Rollback(ctx) + for bucket, count := range counts { + _, err := tx.Exec(ctx, ` + INSERT INTO solana_exec_raw_counts (platform, bucket_start, success_count, updated_at) + VALUES ($1, $2, $3, now()) + ON CONFLICT (platform, bucket_start) DO UPDATE SET + success_count = solana_exec_raw_counts.success_count + EXCLUDED.success_count, + updated_at = now()`, + platform, bucket, count, + ) + if err != nil { + return fmt.Errorf("store: upsert raw count %v: %w", bucket, err) + } + } + return tx.Commit(ctx) +} + +// Materialize recomputes hourly facts for the given platform. +// tx_count comes from solana_exec_raw_counts (full volume); fee metrics come +// from the sampled solana_exec_events (representative quality metrics). +func (db *DB) Materialize(ctx context.Context, platform string) error { + _, err := db.pool.Exec(ctx, ` + INSERT INTO solana_exec_facts + (platform, bucket_start, tx_count, + avg_priority_fee_lamports, p50_cu_price_micro, p95_cu_price_micro, + avg_platform_fee_lamports, jito_rate, avg_cu_consumed, computed_at) + SELECT + e.platform, + date_trunc('hour', e.block_time) AS bucket_start, + COALESCE(r.success_count, COUNT(*)) AS tx_count, + AVG(e.priority_fee_lamports) AS avg_priority_fee_lamports, + COALESCE(percentile_cont(0.5) WITHIN GROUP (ORDER BY e.cu_price_micro) FILTER (WHERE e.cu_price_micro > 0), 0) AS p50_cu_price_micro, + COALESCE(percentile_cont(0.95) WITHIN GROUP (ORDER BY e.cu_price_micro) FILTER (WHERE e.cu_price_micro > 0), 0) AS p95_cu_price_micro, + AVG(e.platform_fee_lamports) AS avg_platform_fee_lamports, + AVG(CASE WHEN e.is_jito_bundle THEN 1.0 ELSE 0.0 END) AS jito_rate, + AVG(e.cu_consumed) AS avg_cu_consumed, + now() + FROM solana_exec_events e + LEFT JOIN solana_exec_raw_counts r + ON r.platform = e.platform + AND r.bucket_start = date_trunc('hour', e.block_time) + WHERE e.platform = $1 + GROUP BY e.platform, date_trunc('hour', e.block_time), r.success_count + ON CONFLICT (platform, bucket_start) DO UPDATE SET + tx_count = EXCLUDED.tx_count, + avg_priority_fee_lamports = EXCLUDED.avg_priority_fee_lamports, + p50_cu_price_micro = EXCLUDED.p50_cu_price_micro, + p95_cu_price_micro = EXCLUDED.p95_cu_price_micro, + avg_platform_fee_lamports = EXCLUDED.avg_platform_fee_lamports, + jito_rate = EXCLUDED.jito_rate, + avg_cu_consumed = EXCLUDED.avg_cu_consumed, + computed_at = now()`, + platform, + ) + return err +} diff --git a/harnesses/solana-exec/migrations/001_init.sql b/harnesses/solana-exec/migrations/001_init.sql new file mode 100644 index 00000000..840ee08b --- /dev/null +++ b/harnesses/solana-exec/migrations/001_init.sql @@ -0,0 +1,49 @@ +BEGIN; + +-- Raw events. One row per transaction that touched a monitored platform fee account. +-- Immutable: only upsert, never update. sig is globally unique on Solana. +CREATE TABLE IF NOT EXISTS solana_exec_events ( + sig TEXT PRIMARY KEY, + platform TEXT NOT NULL, + slot BIGINT NOT NULL, + block_time TIMESTAMPTZ NOT NULL, + -- fee = base + priority. Base ≈ 5000 lamports × num_signers. + total_fee_lamports BIGINT NOT NULL DEFAULT 0, + priority_fee_lamports BIGINT NOT NULL DEFAULT 0, + -- SOL transferred to the platform's fee account in this tx. + platform_fee_lamports BIGINT NOT NULL DEFAULT 0, + is_jito_bundle BOOLEAN NOT NULL DEFAULT false, + jito_tip_lamports BIGINT NOT NULL DEFAULT 0, + cu_consumed BIGINT NOT NULL DEFAULT 0, + -- microlamports per CU (from ComputeBudget instruction if present) + cu_price_micro BIGINT NOT NULL DEFAULT 0, + ingested_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS solana_exec_events_platform_bt + ON solana_exec_events (platform, block_time DESC); + +-- Derived hourly facts. Recomputable from solana_exec_events at any time. +CREATE TABLE IF NOT EXISTS solana_exec_facts ( + platform TEXT NOT NULL, + bucket_start TIMESTAMPTZ NOT NULL, -- truncated to 1h + tx_count INT NOT NULL DEFAULT 0, + avg_priority_fee_lamports DOUBLE PRECISION NOT NULL DEFAULT 0, + p50_cu_price_micro DOUBLE PRECISION NOT NULL DEFAULT 0, + p95_cu_price_micro DOUBLE PRECISION NOT NULL DEFAULT 0, + avg_platform_fee_lamports DOUBLE PRECISION NOT NULL DEFAULT 0, + jito_rate DOUBLE PRECISION NOT NULL DEFAULT 0, + avg_cu_consumed DOUBLE PRECISION NOT NULL DEFAULT 0, + computed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (platform, bucket_start) +); + +-- Cursor per platform: tracks last-seen signature to enable incremental polling. +CREATE TABLE IF NOT EXISTS solana_exec_cursors ( + platform TEXT PRIMARY KEY, + last_sig TEXT NOT NULL, + slot BIGINT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +COMMIT; diff --git a/harnesses/solana-exec/migrations/002_raw_counts.sql b/harnesses/solana-exec/migrations/002_raw_counts.sql new file mode 100644 index 00000000..bf8f199a --- /dev/null +++ b/harnesses/solana-exec/migrations/002_raw_counts.sql @@ -0,0 +1,18 @@ +BEGIN; + +-- Hourly raw successful-tx counts derived from getSignaturesForAddress pagination. +-- Counted at zero extra Helius credit cost (no enhanced API needed). +-- Separate from solana_exec_events which only holds fee-quality sampled rows. +CREATE TABLE IF NOT EXISTS solana_exec_raw_counts ( + platform TEXT NOT NULL, + bucket_start TIMESTAMPTZ NOT NULL, + success_count BIGINT NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (platform, bucket_start) +); + +-- Replace tx_count (INT) in facts with BIGINT to hold real volumes. +ALTER TABLE solana_exec_facts + ALTER COLUMN tx_count TYPE BIGINT; + +COMMIT; 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/app/apps/exec/page.tsx b/src/app/apps/exec/page.tsx new file mode 100644 index 00000000..378bb644 --- /dev/null +++ b/src/app/apps/exec/page.tsx @@ -0,0 +1,77 @@ +import type { Metadata } from "next"; +import { pageMetadata } from "@/lib/page-metadata"; +import { safeJsonLd, buildBreadcrumbJsonLd } from "@/lib/jsonld"; +import { SITE } from "@/data/site"; +import { fetchExecLeaderboard } from "@/lib/solana-exec"; +import { SolanaExecTable } from "@/components/solana-exec-table"; + +const DESCRIPTION = + "Solana trading platform execution quality: priority fees, Jito bundle rates, and platform fees — measured passively from on-chain data. Updated every hour."; + +export const metadata: Metadata = pageMetadata({ + path: "/apps/exec", + title: "Solana Execution Quality — pump.fun, FOMO, Axiom, GMGN | OpenChainBench", + description: DESCRIPTION, +}); + +export const revalidate = 60; + +export default async function SolanaExecPage() { + const data = await fetchExecLeaderboard(); + + const breadcrumb = { + "@context": "https://schema.org", + ...buildBreadcrumbJsonLd([ + { name: "Home", item: SITE.url }, + { name: "Apps", item: `${SITE.url}/apps` }, + { name: "Solana Execution", item: `${SITE.url}/apps/exec` }, + ]), + }; + + return ( +
+