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
28 changes: 2 additions & 26 deletions benchmarks/aggregator-head-lag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ abstract: |
same event.

methodology:
- "Aggregators measured: Mobula, Codex, GeckoTerminal, Serialized."
- "Aggregators measured: Mobula, Codex, GeckoTerminal."
- "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: 4 aggregators × 4 chains × 3 regions = 48 active series."
- "Cardinality: 3 aggregators × 4 chains × 3 regions = 36 active series."

findings:
- "Cross-chain average puts {{best_name}} on top at {{best_p50}} (p50, 24h) across {{count}} providers. Per-chain leaders differ: Base {{best_name:chain:base}} at {{best_p50:chain:base}}, Solana {{best_name:chain:solana}} at {{best_p50:chain:solana}}, BNB {{best_name:chain:bnb}} at {{best_p50:chain:bnb}}, Robinhood {{best_name:chain:robinhood}} at {{best_p50:chain:robinhood}}. Chains differ in block cadence (Solana 400ms vs Base/BNB 2-3s), which shifts the floor independently of provider speed."
Expand Down Expand Up @@ -217,30 +217,6 @@ providers:
p50: avg by (aggregator) (quantile_over_time(0.50, head_lag_seconds{aggregator="codex", region="sgp"}[24h])) * 1000
series: avg_over_time(head_lag_seconds{aggregator="codex", region="sgp"}[1h]) * 1000 unless (changes(head_lag_seconds{aggregator="codex", region="sgp"}[15m]) == 0 and count_over_time(head_lag_seconds{aggregator="codex", region="sgp"}[15m]) > 5 and on(chain, region) sum by (chain, region) (changes(head_lag_seconds{region="sgp"}[15m])) > 5 and on(aggregator, region) avg_over_time(ws_connected{aggregator="codex", region="sgp", chain=""}[15m]) > 0.8)

- slug: serialized
name: Serialized
tag: WebSocket trades stream
formula: "Median seconds between an on-chain swap on the bench pools and the same trade arriving on Serialized's WebSocket trades stream (subscribed per pool by its token side), sampled every 15s over 24h."
queries:
p50: avg by (aggregator) (quantile_over_time(0.50, head_lag_seconds{aggregator="serialized"}[24h])) * 1000
p90: avg by (aggregator) (quantile_over_time(0.90, head_lag_seconds{aggregator="serialized"}[24h])) * 1000
p99: avg by (aggregator) (quantile_over_time(0.99, head_lag_seconds{aggregator="serialized"}[24h])) * 1000
mean: avg by (aggregator) (avg_over_time(head_lag_seconds{aggregator="serialized"}[24h])) * 1000
success: clamp_max(avg by (aggregator) (count_over_time(head_lag_seconds{aggregator="serialized"}[24h]) / 5760), 1)
sample_size: sum(count_over_time(head_lag_seconds{aggregator="serialized"}[24h]))
series: avg_over_time(head_lag_seconds{aggregator="serialized"}[1h]) * 1000 unless (changes(head_lag_seconds{aggregator="serialized"}[15m]) == 0 and count_over_time(head_lag_seconds{aggregator="serialized"}[15m]) > 5 and on(chain, region) sum by (chain, region) (changes(head_lag_seconds{}[15m])) > 5 and on(aggregator, region) avg_over_time(ws_connected{aggregator="serialized", chain=""}[15m]) > 0.8)
live_activity: sum(changes(head_lag_seconds{aggregator="serialized"}[15m]))
regions:
- region: us-east
p50: avg by (aggregator) (quantile_over_time(0.50, head_lag_seconds{aggregator="serialized", region="us-east"}[24h])) * 1000
series: avg_over_time(head_lag_seconds{aggregator="serialized", region="us-east"}[1h]) * 1000 unless (changes(head_lag_seconds{aggregator="serialized", region="us-east"}[15m]) == 0 and count_over_time(head_lag_seconds{aggregator="serialized", region="us-east"}[15m]) > 5 and on(chain, region) sum by (chain, region) (changes(head_lag_seconds{region="us-east"}[15m])) > 5 and on(aggregator, region) avg_over_time(ws_connected{aggregator="serialized", region="us-east", chain=""}[15m]) > 0.8)
- region: eu-west
p50: avg by (aggregator) (quantile_over_time(0.50, head_lag_seconds{aggregator="serialized", region="eu-west"}[24h])) * 1000
series: avg_over_time(head_lag_seconds{aggregator="serialized", region="eu-west"}[1h]) * 1000 unless (changes(head_lag_seconds{aggregator="serialized", region="eu-west"}[15m]) == 0 and count_over_time(head_lag_seconds{aggregator="serialized", region="eu-west"}[15m]) > 5 and on(chain, region) sum by (chain, region) (changes(head_lag_seconds{region="eu-west"}[15m])) > 5 and on(aggregator, region) avg_over_time(ws_connected{aggregator="serialized", region="eu-west", chain=""}[15m]) > 0.8)
- region: ap-southeast
p50: avg by (aggregator) (quantile_over_time(0.50, head_lag_seconds{aggregator="serialized", region="sgp"}[24h])) * 1000
series: avg_over_time(head_lag_seconds{aggregator="serialized", region="sgp"}[1h]) * 1000 unless (changes(head_lag_seconds{aggregator="serialized", region="sgp"}[15m]) == 0 and count_over_time(head_lag_seconds{aggregator="serialized", region="sgp"}[15m]) > 5 and on(chain, region) sum by (chain, region) (changes(head_lag_seconds{region="sgp"}[15m])) > 5 and on(aggregator, region) avg_over_time(ws_connected{aggregator="serialized", region="sgp", chain=""}[15m]) > 0.8)

- slug: geckoterminal
name: GeckoTerminal
tag: REST feed
Expand Down
51 changes: 49 additions & 2 deletions docs/methodology/serialized-onboarding-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
> **Pre-onboarding evaluation.** Run before Serialized is wired into any live harness, so the
> decision to include or exclude them on each bench is documented and reproducible.
>
> **Version:** v1.3, 2026-09-06 (§8 corrected in v1.1; §16.3 root cause corrected and §17 added in v1.2; §18 added in v1.3: Serialized wired into bench 001). Author: internal. Key used: tenant `OpenChainBench`,
> **Version:** v1.4, 2026-09-08 (§8 corrected in v1.1; §16.3 root cause corrected and §17 added in v1.2; §18 added in v1.3: Serialized wired into bench 001; §18.1 added in v1.4: that wiring reverted after measurement). Author: internal. Key used: tenant `OpenChainBench`,
> plan `starter`, keyId `d5511a080aaa`, issued 2026-09-04.

---
Expand Down Expand Up @@ -503,7 +503,7 @@ Layer 3, the real bench: snapshot every provider's verdict at mint, resolve on-c
Snapshot `top10HoldersPct` and `bundlersHoldingsPct` as the primary signals per §17.5.


## 18. Bench 001: Serialized is in (v1.3, 2026-09-06)
## 18. Bench 001: Serialized wired, then pulled (v1.4, 2026-09-08)

Earlier sections called 001 "blocked on a policy decision". That was the wrong framing, and it hid a
practical question nobody had tested: does Serialized's stream cover the four bench pools at all?
Expand Down Expand Up @@ -533,3 +533,50 @@ Solana attempt looked like a failure.
`head_lag_ref_seconds` against the node reference clock matched by `txHash`, which is the one that can
rank providers. Every event carries `txHash`, `block`, `poolAddress` and a `preconfirmed` boolean per
their docs, so Base flashblocks are visible rather than inferred.

### 18.1 Why it was reverted

Measured 2026-09-08 before promoting the wiring to `main`. One WS connection, the four bench pools,
a 240 s capture, plus a targeted 150 s run that asked a public Base node whether the block existed
at the moment each trade arrived. Single vantage (workstation, NTP offset +0.078 s), so absolute
latencies are not comparable to harness numbers; the signs and the orders of magnitude are.

**The ruler matches.** Their `at` is the block timestamp exactly, so `head_lag_seconds` would measure
the same quantity as Mobula's `trade.Date` and Codex's `event.Timestamp`:

| chain | n | median(`at` − block timestamp) |
|---|---|---|
| base | 105 | +0.000 s |
| bnb | 1 | +0.000 s |
| solana | 28 | +0.473 s (artefact: `getBlockTime` returns whole seconds) |

**But they read a different thing.** On Base, every sampled trade arrived before its own block:

| check | result |
|---|---|
| median(receipt − block timestamp), base | −0.911 s |
| share of base events with negative lag | 105/105 (100%) |
| trades received before the block existed on a public node | 11/11 |
| median lead over block publication | 1.04 s |

In all eleven cases the node head at receipt was exactly `target block − 1`: the block was not late in
propagation, it did not exist yet. The spread (−0.21 s to −1.80 s) is the width of one Base slot.
Serialized streams sequencer preconfirmations; Mobula, Codex and GeckoTerminal read sealed blocks.

**Decision: excluded from bench 001.** Not because the measurement is wrong, but because the two
emissions are different products. A preconfirmation carries no finality guarantee and can be
reordered, so the ~1.5 s lead is a latency/finality trade-off, not pure speed. Publishing both in one
ranking would present that trade-off as superiority. `RecordHeadLag` also drops negatives outright
(`metrics.go`), so on the legacy series they would show as no data on Base while being the fastest —
and any rare positive sample would stick on the gauge and become their published p50.

Two findings worth raising with them:

- `preconfirmed` was `false` on all eleven events that preceded block publication. A consumer cannot
tell the two regimes apart from the payload.
- Robinhood delivered **0 events in 240 s** despite an acknowledged subscription, against 79 in 90 s
recorded on 2026-09-06 (§18 table). Re-verify before any future wiring.

**Reopen if** the bench gains an emission-regime dimension (sealed vs preconfirmed), or a landing-rate
companion series shows their preconfirmations reach sealed blocks 1:1. Serialized stays on benches
004, 005, 008 and 090, where the comparison is like for like.
2 changes: 0 additions & 2 deletions harnesses/aggregator-head-lag/cmd/script/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
type Config struct {
CoinGeckoAPIKey string
MobulaAPIKey string
SerializedAPIKey string
DefinedSessionCookie string
MonitorRegion string // Deployment region: us-west, us-east, singapore, etc.
MobulaWSURL string // Mobula fast-trade WebSocket endpoint (allows staging to use EU-specific cluster)
Expand All @@ -22,7 +21,6 @@ func loadEnv() (*Config, error) {
// First, try to load from environment variables (for production/Railway)
config.CoinGeckoAPIKey = strings.TrimSpace(os.Getenv("COINGECKO_API_KEY"))
config.MobulaAPIKey = strings.TrimSpace(os.Getenv("MOBULA_API_KEY"))
config.SerializedAPIKey = strings.TrimSpace(os.Getenv("SERIALIZED_API_KEY"))
config.DefinedSessionCookie = strings.TrimSpace(os.Getenv("DEFINED_SESSION_COOKIE"))
config.MonitorRegion = strings.TrimSpace(os.Getenv("MONITOR_REGION"))
config.MobulaWSURL = strings.TrimSpace(os.Getenv("MOBULA_WS_URL"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -811,9 +811,6 @@ func runHeadLagMonitor(config *Config, stopChan <-chan struct{}) {
wg.Add(1)
go runGeckoTerminalHeadLagMonitor(config, stopChan, &wg)

wg.Add(1)
go runSerializedHeadLagMonitor(config, stopChan, &wg)

// Wait for all to finish
wg.Wait()
fmt.Println("[HEAD-LAG] All monitors stopped")
Expand Down
Loading
Loading