diff --git a/benchmarks/aggregator-head-lag.yml b/benchmarks/aggregator-head-lag.yml index 39fdfe0c..e72b0c81 100644 --- a/benchmarks/aggregator-head-lag.yml +++ b/benchmarks/aggregator-head-lag.yml @@ -5,7 +5,7 @@ number: "001" title: Fastest crypto price API, live head lag across Mobula, Codex, GeckoTerminal seo_title: "Fastest crypto price API 2026" seo_description: "{{best_name}} leads fastest crypto price API at {{best_p50}} (p50, 24h). Mobula WebSocket, Codex GraphQL, GeckoTerminal REST live across Base, BNB, Solana." -subtitle: Wall-clock head lag in seconds from on-chain swap event to API emission, measured live for Mobula, Codex and GeckoTerminal on Base, BNB Chain and Solana. +subtitle: Wall-clock head lag in seconds from on-chain swap event to API emission, measured live for Mobula, Codex and GeckoTerminal on Base, BNB Chain, Solana and Robinhood Chain. per_chain_explainer: - slug: base @@ -20,6 +20,10 @@ per_chain_explainer: h2: "Lowest Solana price feed head lag" body: | {{best_name:chain:solana}} currently leads Solana head lag at {{best_p50:chain:solana}} (p50, 24h). Solana runs a 400 ms slot under Proof of History with confirmed status at typically 1-2 slots, so any second-scale provider latency lives entirely in the indexer pipeline rather than in the chain. Providers that subscribe to Yellowstone gRPC streams of confirmed blocks beat REST-pollers by definition. Measured against a canonical archive node from us-east, eu-west and Singapore. + - slug: robinhood + h2: "Lowest Robinhood Chain price feed head lag" + body: | + {{best_name:chain:robinhood}} currently leads Robinhood Chain head lag at {{best_p50:chain:robinhood}} (p50, 24h). Robinhood Chain is an Arbitrum Orbit L2 with sub-second block cadence, so nearly all of the latency you see here is provider pipeline (indexer, fan-out, edge) rather than chain time. It is also the youngest chain on this bench: indexer coverage is uneven across providers, which makes the leader-to-trailer spread wider than on mature chains. Measured on the highest-activity pool (USDG/WETH, about 40k swaps per day) from us-east, eu-west and Singapore. category: Aggregators status: live @@ -35,7 +39,7 @@ seo_intro: | number. Mobula's WebSocket, Codex's GraphQL feed and GeckoTerminal's REST endpoint are watched from three regions (us-east, eu-west, sgp) against canonical-tip archive - nodes on Base, BNB Chain and Solana. Because each chain has its own + nodes on Base, BNB Chain, Solana and Robinhood Chain. Because each chain has its own base block cadence (Solana's ~400 ms slots vs Base/BNB's 2-3 s blocks), the headline winner shifts by chain. On Solana the leader is {{best_name:chain:solana}} at {{best_p50:chain:solana}}; on Base @@ -51,19 +55,19 @@ seo_intro: | abstract: | We measure the gap between a transaction settling on chain and the same transaction appearing on each aggregator's data feed. The harness watches - reference pools across Base, BNB Chain and Solana from three regions + reference pools across Base, BNB Chain, Solana and Robinhood Chain from three regions (us-east, eu-west, sgp), records every new on-chain event with millisecond timestamps, and observes when each aggregator's WebSocket reflects the same event. methodology: - "Aggregators measured: Mobula, Codex, GeckoTerminal." - - "Chains: Base, BNB Chain, Solana." + - "Chains: Base, BNB Chain, Solana, Robinhood Chain." - "Regions: us-east, eu-west, sgp. Cross-region median reported in the headline." - "Reference: archive nodes per chain, validated against block hashes." - "Metric: gauge `head_lag_seconds`, sampled every 15 seconds. Aggregated over the 24-hour window using `quantile_over_time`." - "Success rate: presence ratio. Share of expected sampling slots where a value was actually emitted (5,760 expected per provider per day at the 15s cadence). 100% means the aggregator's feed was reachable for the full window." - - "Cardinality: 3 aggregators × 3 chains × 3 regions = 27 active series." + - "Cardinality: 3 aggregators × 4 chains × 3 regions = 36 active series." findings: - "Across the unfiltered cross-chain aggregate the leader is {{best_name}} at {{best_p50}} (p50, 24 h) over {{count}} measured providers, but that headline number mixes chains with very different baseline cadences (Solana 400 ms slots vs Base/BNB 2-3 s blocks) so the per-chain breakdown below is what to read for an integration decision." @@ -90,7 +94,7 @@ faq: - q: "How fast does GeckoTerminal update prices?" a: "{{name:geckoterminal}} currently sits at {{p50:geckoterminal}} (p50, 24 h) head lag on this benchmark. REST endpoints update at the publisher's chosen cadence rather than the moment a swap lands, which is the architectural reason REST polling reads slower than WebSocket feeds for the same underlying chain data." - q: "How is head lag measured on OpenChainBench?" - a: "The harness holds a live WebSocket subscription to canonical-tip archive nodes on each chain (Base, BNB Chain, Solana). Every new on-chain swap is timestamped on receipt with millisecond precision. The harness then watches each provider's feed for the same event and computes the delta. p50, p90 and p99 are aggregated over 24 hours via Prometheus `quantile_over_time`. Errors and dropouts are tracked separately as a presence ratio so a fast feed that disappears does not silently rank above a slower feed that stays up." + a: "The harness holds a live WebSocket subscription to canonical-tip archive nodes on each chain (Base, BNB Chain, Solana, Robinhood Chain). Every new on-chain swap is timestamped on receipt with millisecond precision. The harness then watches each provider's feed for the same event and computes the delta. p50, p90 and p99 are aggregated over 24 hours via Prometheus `quantile_over_time`. Errors and dropouts are tracked separately as a presence ratio so a fast feed that disappears does not silently rank above a slower feed that stays up." - q: "Which data provider should I pick for a real-time trading interface?" a: "Read both the p50 and the p99 columns. p50 tells the typical experience, p99 tells the worst minute a user will see when a chain spikes or a region's path degrades. A 500 ms p50 with a 30 s p99 means most of the time the UI feels snappy and once per minute it stutters. The leaderboard splits both, plus the success rate column flags providers whose feed dropped during the measurement window." @@ -99,10 +103,11 @@ faq: # every PromQL selector via lib/spec.ts:injectChainFilter. dimensions: chain: - - { value: all, label: All chains } - - { value: base, label: Base } - - { value: bnb, label: BNB Chain } - - { value: solana, label: Solana } + - { value: all, label: All chains } + - { value: base, label: Base } + - { value: bnb, label: BNB Chain } + - { value: solana, label: Solana } + - { value: robinhood, label: Robinhood } # Region values match the raw harness labels (REGION env on each # Railway probe: us-east / eu-west / sgp). Declared so the # /compare/[slug] page can render a per chain x per region matrix diff --git a/harnesses/aggregator-head-lag/cmd/script/geckoterminal_monitor.go b/harnesses/aggregator-head-lag/cmd/script/geckoterminal_monitor.go index f47856f0..89d28b70 100644 --- a/harnesses/aggregator-head-lag/cmd/script/geckoterminal_monitor.go +++ b/harnesses/aggregator-head-lag/cmd/script/geckoterminal_monitor.go @@ -52,6 +52,15 @@ var geckoTerminalPools = []struct { PoolID: "160787671", Chain: "bnb", }, + { + // Highest txn-frequency pool on Robinhood Chain (~40k swaps/24h). + // Internal GT pool_id source: app.geckoterminal.com/api/p1/robinhood/pools/
+ // Pool address: 0x69bfaf19c9f377bb306a89aed9f6b07e2c1a8d9a + Name: "USDG/WETH Robinhood", + Network: "robinhood", + PoolID: "328167634", + Chain: "robinhood", + }, } // ActionCable message structures 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 dc9b04d9..b52a9f12 100644 --- a/harnesses/aggregator-head-lag/cmd/script/head_lag_monitor.go +++ b/harnesses/aggregator-head-lag/cmd/script/head_lag_monitor.go @@ -56,6 +56,14 @@ var headLagPools = []HeadLagPool{ Address: "0x58f876857a02d6762e0101bb5c46a8c1ed44dc16", ChainName: "bnb", }, + { + // Highest txn-frequency pool on Robinhood Chain (~40k swaps/24h). + Name: "USDG/WETH Robinhood", + Blockchain: "evm:4663", + NetworkID: 4663, + Address: "0x69bfaf19c9f377bb306a89aed9f6b07e2c1a8d9a", + ChainName: "robinhood", + }, } // ============================================================================ @@ -277,6 +285,8 @@ func getChainNameFromBlockchain(blockchain string) string { return "base" case "BNB Smart Chain (BEP20)", "BSC", "evm:56": return "bnb" + case "Robinhood Chain", "evm:4663": + return "robinhood" default: return blockchain } @@ -456,15 +466,18 @@ func connectAndMonitorCodex(config *Config, stopChan <-chan struct{}) error { var subMsg map[string]interface{} - // Solana: use onUnconfirmedEventsCreated (pre-finalized, lowest latency) + // Solana: commitmentLevel Processed (pre-finalized, lowest latency). + // This replaces onUnconfirmedEventsCreated, deprecated by Codex on + // 2026-04-20 and enforcement-killed for API keys ~2026-07-06 (hard + // WS close 1006). Verified delivering on the JWT path 2026-07-09. if pool.ChainName == "solana" { poolID := fmt.Sprintf("%s:%d", pool.Address, pool.NetworkID) subMsg = map[string]interface{}{ "type": "subscribe", "id": subID, "payload": map[string]interface{}{ - "query": `subscription OnPoolEvents($id: String!) { - onUnconfirmedEventsCreated(id: $id, quoteToken: token0) { + "query": `subscription OnPoolEvents($id: String!, $cl: [EventCommitmentLevel!]) { + onEventsCreated(id: $id, commitmentLevel: $cl) { address networkId events { @@ -477,6 +490,7 @@ func connectAndMonitorCodex(config *Config, stopChan <-chan struct{}) error { }`, "variables": map[string]interface{}{ "id": poolID, + "cl": []string{"Processed"}, }, }, } @@ -510,40 +524,6 @@ func connectAndMonitorCodex(config *Config, stopChan <-chan struct{}) error { return fmt.Errorf("subscribe to %s failed: %w", pool.Name, err) } - // onUnconfirmedEventsCreated is deprecated (Codex changelog 2026-04-20) - // and already hard-killed for API-key auth since ~07-06. Subscribe the - // replacement (onEventsCreated + commitmentLevel Processed) in parallel: - // logs tell us which one actually delivers on the JWT path, and we keep - // data flowing whichever side Codex cuts next. Gauge takes last write, - // so double delivery is harmless. - if pool.ChainName == "solana" { - procMsg := map[string]interface{}{ - "type": "subscribe", - "id": fmt.Sprintf("headlag_proc_%d", i), - "payload": map[string]interface{}{ - "query": `subscription OnPoolEvents($id: String!, $cl: [EventCommitmentLevel!]) { - onEventsCreated(id: $id, commitmentLevel: $cl) { - address - networkId - events { - blockNumber - timestamp - transactionHash - eventType - } - } - }`, - "variables": map[string]interface{}{ - "id": fmt.Sprintf("%s:%d", pool.Address, pool.NetworkID), - "cl": []string{"Processed"}, - }, - }, - } - if err := conn.WriteJSON(procMsg); err != nil { - return fmt.Errorf("processed subscribe to %s failed: %w", pool.Name, err) - } - } - time.Sleep(100 * time.Millisecond) // Small delay between subscriptions } @@ -660,13 +640,6 @@ func connectAndMonitorCodex(config *Config, stopChan <-chan struct{}) error { // Codex killed the Solana sub exactly this way. Force a full // reconnect + resubscribe instead. if wsMsg.Type == "complete" || wsMsg.Type == "error" { - // The experimental Processed sub failing must not tear down the - // legacy flow: log it and keep the connection alive. - if strings.HasPrefix(wsMsg.ID, "headlag_proc_") { - payloadStr, _ := json.Marshal(wsMsg.Payload) - log.Printf("[HEAD-LAG][CODEX] ⚠️ Processed sub %q terminated (type=%s payload=%s) — legacy sub continues", wsMsg.ID, wsMsg.Type, string(payloadStr)) - continue - } return fmt.Errorf("subscription %q terminated by server (type=%s)", wsMsg.ID, wsMsg.Type) } @@ -762,6 +735,8 @@ func getChainNameFromNetworkID(networkID int) string { return "base" case 56: return "bnb" + case 4663: + return "robinhood" default: return fmt.Sprintf("network_%d", networkID) } diff --git a/harnesses/aggregator-head-lag/cmd/script/mobula_fast_trade_monitor.go b/harnesses/aggregator-head-lag/cmd/script/mobula_fast_trade_monitor.go index 9629ef2e..d45e0ef6 100644 --- a/harnesses/aggregator-head-lag/cmd/script/mobula_fast_trade_monitor.go +++ b/harnesses/aggregator-head-lag/cmd/script/mobula_fast_trade_monitor.go @@ -45,6 +45,11 @@ var fastTradePools = []struct { Address: "0x58f876857a02d6762e0101bb5c46a8c1ed44dc16", // WBNB/BUSD PancakeSwap ChainName: "bnb", }, + { + Blockchain: "evm:4663", + Address: "0x69bfaf19c9f377bb306a89aed9f6b07e2c1a8d9a", // USDG/WETH Robinhood + ChainName: "robinhood", + }, } func runMobulaFastTradeMonitor(config *Config, stopChan <-chan struct{}) { @@ -219,6 +224,8 @@ func getChainNameFromBlockchainFastTrade(blockchain string) string { return "base" case "BNB Smart Chain (BEP20)", "BSC", "evm:56": return "bnb" + case "Robinhood Chain", "evm:4663": + return "robinhood" default: return blockchain } diff --git a/infrastructure/monitoring/prometheus/alert_rules.yml b/infrastructure/monitoring/prometheus/alert_rules.yml index 561cca64..f06632b1 100644 --- a/infrastructure/monitoring/prometheus/alert_rules.yml +++ b/infrastructure/monitoring/prometheus/alert_rules.yml @@ -96,7 +96,10 @@ groups: # harness qui ré-expose la dernière valeur) a des samples frais à # chaque scrape — timestamp() ne voit rien (Codex Solana 2026-07-09, # 8h de 26.3s figés sans alerte). Un vrai flux a toujours du jitter. - expr: changes(head_lag_seconds[30m]) == 0 + # Scope mobula uniquement: un feed tiers (codex/gecko) qui meurt est + # leur histoire, le watchdog purge ses gauges et la page montre le + # trou. Pas de bruit Slack pour eux. + expr: changes(head_lag_seconds{aggregator="mobula"}[30m]) == 0 for: 1m labels: severity: warning @@ -160,7 +163,8 @@ groups: # Aggregator WS silently down: connection-state gauge from the # reconnect instrumentation (PR #950). - alert: AggregatorWSDisconnected - expr: ws_connected == 0 + # mobula only, même logique que AggregatorHeadLagStale. + expr: ws_connected{aggregator="mobula"} == 0 for: 10m labels: severity: warning diff --git a/public/logos/robinhood.png b/public/logos/robinhood.png new file mode 100644 index 00000000..2746f39b Binary files /dev/null and b/public/logos/robinhood.png differ diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index 433f4e77..d580c026 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -43,6 +43,7 @@ const RAW: Record