Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions benchmarks/aggregator-head-lag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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."
Expand All @@ -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."

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/<address>
// Pool address: 0x69bfaf19c9f377bb306a89aed9f6b07e2c1a8d9a
Name: "USDG/WETH Robinhood",
Network: "robinhood",
PoolID: "328167634",
Chain: "robinhood",
},
}

// ActionCable message structures
Expand Down
63 changes: 19 additions & 44 deletions harnesses/aggregator-head-lag/cmd/script/head_lag_monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}

// ============================================================================
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand All @@ -477,6 +490,7 @@ func connectAndMonitorCodex(config *Config, stopChan <-chan struct{}) error {
}`,
"variables": map[string]interface{}{
"id": poolID,
"cl": []string{"Processed"},
},
},
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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{}) {
Expand Down Expand Up @@ -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
}
Expand Down
8 changes: 6 additions & 2 deletions infrastructure/monitoring/prometheus/alert_rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Binary file added public/logos/robinhood.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/lib/logo-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const RAW: Record<string, string> = {
sonic: "/logos/sonic.png",
monad: "/logos/monad.png",
megaeth: "/logos/megaeth.png",
robinhood: "/logos/robinhood.png",
onfinality: "/logos/onfinality.png",
berachain: "/logos/berachain.png",

Expand Down
Loading