drop journal framing#1
Merged
Merged
Conversation
4 tasks
Flotapponnier
added a commit
that referenced
this pull request
May 21, 2026
Pre-registered methodology v1.1 + launch plan (5 services x us-east x 1h cadence, ~$159/mo). Required by Rule #1 to be merged before any sponsor contract. - methodology/solana-tx-landing-active.md: probe payload, submission flow, drop reasons, sponsor-proof guarantees, versioning - solana-landing-tiered-architecture.md: V0-Lean spec + escalation tiers (V0.5, V1, V2) - solana-landing-cost-model.md: probe cost formula + tip-floor analysis (note flagging arithmetic error in scenarios table) - solana-landing-deployment-checklist.md: 8-step Florent runbook (keypair, API keys, Railway env, sponsor outreach) - solana-landing-active-bench-plan.md: redirect to current docs
This was referenced May 21, 2026
Flotapponnier
added a commit
that referenced
this pull request
May 22, 2026
…xes (#83) YAML rewrites (GSC-driven): - l1-finality: title leads with Ethereum 12.8min (the #1 query at 10 imp/wk), no longer Stellar - bridge-quote-latency: drop benchmark 2026 filler, front-load ms unit - metadata-coverage: drop 'Best X in 2026' listicle pattern, lead with concrete artifacts (logo/desc/twitter/website) - bridge-fee: drop 'in 2026', lead with cost formula (fees + slippage + gas) - aggregator-head-lag: defensive tighten (description was 263c, truncated in SERP) - l2-block-time: trim description from 372 chars (violated schema 320 cap) TSX bug fixes: - alternatives/[slug] BreadcrumbList: position-2 crumb pointed to /benchmarks (wrong) - dropped (no /alternatives index page exists; 2-level breadcrumb is valid) - layout.tsx Organization sameAs: removed fabricated reddit.com/r/openchainbench URL (broken sameAs = soft-fail per Google validator) Expected weekly clicks: 8 -> 37-53 (+30 to +45). l1-finality alone projected 1 -> 25-35 clicks/wk.
This was referenced May 22, 2026
Flotapponnier
added a commit
that referenced
this pull request
May 23, 2026
) Provider lifecycle When prom returns null for a provider's p50/p90/p99 queries this cycle, spec.ts now tags the augmented entry with availability='unavailable' instead of leaving it as a row of zeros that ranks #1 on lower-is-better benches (the 0slot / cardano UX bug). LedgerTable renders the row at the bottom with a soft amber pill 'Currently unavailable' + 'Awaiting next successful scrape'. Stats helpers (computeFieldStats, rankProviders, benchmark-body, chain-headings-summary, bench-template, citation) all exclude unavailable rows so Best/Median/Spread no longer collapse to 0. Slack alerting New /api/cron/health-check route. For each live bench it asks prom two questions per provider: 'did you receive samples in the last 10 min?' and the same query offset 6 min back. The edge (was-live xor is-live) triggers a slack post via SLACK_WEBHOOK_URL env. No external state - we use prom's own series history as the source of truth. Operator setup (one-time, in Vercel project settings) - SLACK_WEBHOOK_URL : the incoming-webhook url (channel of choice) - CRON_SECRET : random string, used to gate the cron route vercel.json schedules the route every 5 minutes. Vercel injects the Authorization: Bearer $CRON_SECRET header automatically.
Flotapponnier
added a commit
that referenced
this pull request
May 23, 2026
5 most-visible tooltip surfaces now use the Hint primitive instead of the browser's default 800 ms title= popup: - home-bench-table — provider initials chips on every home card - benchmark-card — provider initials chips on the /benchmarks grid - distribution-chart — p50 / p90 / p99 percentile markers - ranked-bar-chart — provider name / tag row chip - ledger-table — 'Currently unavailable' badge (had a 2-sentence paragraph stuffed inside a title= attribute, illegible) Hint already shipped in the previous PR; this batch attaches it where the design audit identified the worst UX gaps (provider chips were the agent's #1 example: user sees a 'C' and has to wait 800 ms for the browser to maybe show 'Coinbase'). 120 ms hover delay, instant on focus, ink-on-paper styled - matches Stripe / Linear / Vercel conventions for read-only dashboards. Row-level tooltips (full <li> with methodologyTooltip) keep their native title= for now - wrapping <li> in <Hint>'s <span> would break <ul> semantics. Migrating those needs a small Hint asChild pattern (radix-style cloneElement); deferred to a focused follow-up PR if visible enough to warrant it.
This was referenced May 29, 2026
Flotapponnier
added a commit
that referenced
this pull request
May 29, 2026
…-only fix(ledger): only render #1 chips on the leaderboard row
Flotapponnier
added a commit
that referenced
this pull request
Jun 1, 2026
…, outage, taker, per-asset) (#248) Replace the old findings block (template-based, generic) with 6 new findings that cite specific live numbers from the v2 metrics: - effective fee spread 10x across cohort (Insilico 1.00 → MetaMask 10.00) - price deviation as user-cost signal (Dexari 25.83 bps, 13x median) - volume ≠ user-friendliness (Phantom #1 volume but Insilico cheapest) - two live outage signals (Dexari 19min stale, Okto 24h inactive) - taker share separates aggressive UIs from pro terminals - asset concentration reveals routing strategy (HIP-3 stocks on Dexari/MM) Plus 2 new FAQ entries explaining what price_deviation and last_fill_age actually measure, since they are unique to this bench in the HL space.
Flotapponnier
added a commit
that referenced
this pull request
Jun 2, 2026
* chore(obs): temporary [DRAFT-TRACE] logs on bench draft fallback paths (#147)
Logs every fork that produces a visible draft render to the user:
kv_hit / kv_miss / kv_empty / kv_shape / kv_stale / kv_timeout /
kv_neterr / kv_http / collapse / placeholder_used
Includes slug + timing per call. All prefixed with [DRAFT-TRACE] for
easy grep in Vercel logs. Plain console.warn — no extra deps.
Revert this commit once we have the cause pinned.
* fix(harness): bump Astralane default tip 500k → 1M lamports (#148)
* chore(obs): temporary [DRAFT-TRACE] logs on bench draft fallback paths
Logs every fork that produces a visible draft render to the user:
kv_hit / kv_miss / kv_empty / kv_shape / kv_stale / kv_timeout /
kv_neterr / kv_http / collapse / placeholder_used
Includes slug + timing per call. All prefixed with [DRAFT-TRACE] for
easy grep in Vercel logs. Plain console.warn — no extra deps.
Revert this commit once we have the cause pinned.
* fix(solana-tx-landing harness): bump Astralane default tip to 1M lamports
Sync from the private mobula-api harness. Astralane raised their min
tip floor 500_000 → 1_000_000; keeps the public harness reproducing
the same probe config.
* fix(perf): bump Prom timeout 4s→10s + cron pre-warms bench cache (#149)
Two root causes pinned via [DRAFT-TRACE] logs on staging:
1. Prom queries quantile_over_time(0.99, …[24h]) consistently take
4-8s under load. The 4s client timeout aborted, collapsing benches
to draft when the per-bench KV snapshot wasn't available as fallback.
Bump to 10s — fits the observed worst case with headroom.
2. The /api/cron/health-check route runs every 5 min but only computed
Slack transitions. Add a loadAllBenchmarks() call so the per-bench
unstable_cache stays warm and writeSnapshot() refreshes the KV
snapshot layer. Eliminates the cold-start window where a fresh
Vercel function instance has no in-memory cache AND the snapshot is
stale or missing — exactly the pattern that produces the visible
'bench in draft' symptom users observed.
* fix(logo): wrap 1RPC / Blocknative / Lava / Tenderly / Jito / Astralane in light chip (#150)
* fix(perf): bump Prom timeout 4s→10s + cron pre-warms bench cache
Two root causes pinned via [DRAFT-TRACE] logs on staging:
1. Prom queries quantile_over_time(0.99, …[24h]) consistently take
4-8s under load. The 4s client timeout aborted, collapsing benches
to draft when the per-bench KV snapshot wasn't available as fallback.
Bump to 10s — fits the observed worst case with headroom.
2. The /api/cron/health-check route runs every 5 min but only computed
Slack transitions. Add a loadAllBenchmarks() call so the per-bench
unstable_cache stays warm and writeSnapshot() refreshes the KV
snapshot layer. Eliminates the cold-start window where a fresh
Vercel function instance has no in-memory cache AND the snapshot is
stale or missing — exactly the pattern that produces the visible
'bench in draft' symptom users observed.
* fix(logo): wrap dark-toned provider logos in light chip
1RPC, Blocknative, Lava, Tenderly, Jito and Astralane have single-tone
(mostly dark) artwork that disappears against the page background on
small bench cards. Add to NEEDS_LIGHT_CHIP so they render on a white
chip with a hairline shadow — same treatment as Relay, Debridge, Tonapi.
* fix(logo): also chip dydx v4 and Sky maker (#151)
Same single-tone artwork pattern as the previous batch — invisible on
the bench card background. Wrap in white chip for contrast.
* fix(logo): chip Merkle, Moralis, Nodies — invisible in dark mode (#152)
* fix(logo): introduce NEEDS_DARK_CHIP for white-fill marks (Jito, Astralane, Sky, dydx) (#153)
Verified via SVG fill audit: those 4 logos have fill="white" content,
which the previous white chip rendered as invisible-on-invisible.
Split into two sets — light chip (#fff for dark logos) and dark chip
(#0f172a for white logos).
* fix(build): tolerate all-bench-draft state instead of crashing build (#154)
The safety throw 'every bench draft, refusing to update cache' crashes
next build when Prom is fully unreachable during the build window.
Replace the throw with a [DRAFT-TRACE] warning + return the draft array.
With the snapshot/KV fallback + 10s Prom timeout + cron pre-warm already
mitigating user-visible drafts, this safety net's value is marginal.
* crop 1rpc/tenderly/lava logos to icon-only (#155)
* replace blocknative logo with cleaner vector version (#156)
* feat(bench-005): add 4 providers to network-coverage (#157)
* feat(bench-005): add coinpaprika, covalent, coinstats, sim-dune to network-coverage
* feat(bench-005): add coinpaprika/covalent/coinstats logos
* fix(bench-005): correct CoinStats endpoint path
* feat(products): add coinpaprika/covalent/coinstats/sim-dune/dune to provider registry + swap dune logo to PNG (#158)
* fix(bench-005): rename sim-dune to dune + sync OSS harness mirror with 4 providers (#159)
* feat(bench-027): add Mobula Swap-Send to solana-tx-landing-latency (#160)
* feat(bench-027): add Mobula Swap-Send to solana-tx-landing-latency providers
* feat(harnesses): sync solana-tx-landing OSS mirror with Mobula provider
* fix(bench-027): trim Mobula formula to 240 chars (Zod schema limit) (#161)
* fix(bench-027): rename Mobula Swap-Send to Mobula in product ledger (#162)
* feat(products): consolidate alias slugs (helius-sender/avalanche-official/oracle pairs) into one canonical entry (#163)
* feat(bench): simplify provider display names to brand-only across benches (#164)
* fix(distribution): auto log scale when dynamic range > 50x
The distribution view used a linear scale anchored at max(p99). On benches
with very wide ranges (l1-finality spans 0.4s on TON to 36 min on Monero,
ratio 5370x) every fast row collapsed to a single invisible pixel at the
left edge, making 7 of 9 chains visually indistinguishable. Only the two
slowest rows had visible bars.
Port the same auto-log heuristic ranked-bar-chart already uses: when
max/min > 50, switch to base-10 log projection so each row gets a
proportional, distinguishable position. Footer flags 'log scale' so
the reader knows when it kicks in.
Same component is shared by every bench in distribution view, so this
fixes l1-finality, l2-block-time, validator-yield, perp-fees and any
other bench with wide dynamic range in one shot.
* fix(distribution): anchor left edge at fieldMin, not absolute zero
Narrow-range benches with small absolute minimums collapse every median
dot into a single sliver at the left. solana-tx-landing-latency is the
canonical case: 5 providers all have p50 = 2 slots on a 14-slot field,
so every median lands at 14 % of the track and there is no visible
spread between the leaders.
Switch both linear and log scale to use fieldMin as the left baseline.
The leader now sits at 0 % and laggards spread across the full track.
Footer always shows 'min X' so the reader knows the chart is relative
to observed range, not absolute zero.
* scrub deploy IDs and team scope from public workflow
* feat(bench-001): add GMGN as a provider (Solana-only) + sync harness
GMGN's WebSocket pushes Solana chain-tip events (channel=chain_stat, gk='s', rs=ms:slot) sub-second. Adds it as the 4th provider on the head-lag bench.
YAML:
- New provider entry; queries deliberately omit chain= so per-tab filters auto-hide GMGN on Base/BNB and surface it on All/Solana.
- Methodology + cardinality lines updated.
Harness sync (matches private mobula-api/miniapps/aggregator-head-lag):
- gmgn_monitor.go: chromedp cf_clearance mint + utls HelloChrome_120 ClientHello (h1-only ALPN) + WS subscribe + slot-event parser.
- Dockerfile: chromium + minimal X/font libs for chromedp.
- go.mod: utls + chromedp deps.
- log_buffer.go, config.go, main.go: GMGN_PROXY env + startup banner + goroutine launch.
Validated: bun run validate, typecheck, spec tests all green.
* chore(harness/aggregator-head-lag): drop committed build binary + gitignore it
* scrub slack webhook and grafana URL from metadata-coverage
* remove alerting from metadata-coverage harness
* restore alerting with env-var webhook (envsubst pattern)
* feat(products): wire GMGN logo + registry entry for /products/gmgn
Auto-renders /products/gmgn with brand logo, description and bench appearances now that GMGN appears in bench-001 results. Logo SVG provided by maintainer (12KB, official asset).
* rename relay-link-revenue bench to bridge-revenue
* add 30d range to time-series chart
* drop relay-link-revenue redirect (low traffic, not worth keeping)
* fix bridge-revenue seo_description over 320-char Zod limit
* rpc-capabilities: per-region metric labels + multi-region YAML aggregation
* rpc-capabilities: auto-detect RAILWAY_REPLICA_REGION + 3-region text in YAML
* rpc-capabilities: per-region queries enable region tabs on chart
* metadata-coverage: per-chain editorial (Solana/BNB leaders explicit)
* bridge-fee: flag cross-corridor aggregate; defer per-corridor rewrite
* bridge-quote-latency: flag cross-corridor aggregate; defer per-corridor rewrite
* rpc-capabilities: per-chain editorial (Ethereum/Base/BNB/Arbitrum leaders explicit)
* seo: chain-aware @id suffix in jsonld + page-metadata helpers
* feat(products): add chain-coverage chip + per-chain contract bridge
* seo: bench generateMetadata reads ?chain= and renders chain-honest title + description
* feat(providers): chain-aware ranking + per-chain win counting
* seo: emit per-chain OG + twitter cards via generateImageMetadata
* feat(badge): accept ?chain= and label scope in SVG
* feat(seo): chain-aware metadata + OG + sitemap + JSON-LD
* feat(products page): render per-chain rank chips and clarify wins column
* feat(products+badge): chain-aware ranking + per-chain badge + coverage chip
* core: bestPerChain + worstPerChain stash with chain-aware helpers
Adds per-chain leader/trailer computation in spec.ts that runs only on
the unfiltered All view of benches with dimensions.chain. Fan out one
extra tryLoadLive per chain (excluding 'all') with the chain label
injected, then stash leader + trailer on the Benchmark object.
Exports bestForChain (lib/spec) and leaderForChain (lib/citation) so
the SEO surface + badge endpoints can pull a chain-specific winner
without recomputing.
* bench-001(yaml): rewrite findings/faq/seo_intro for per-chain leaders
Stops surfacing a chain-mix-biased global winner on copy seen across
the site. findings #1 calls out the unfiltered aggregate is biased,
then per-chain bullets break out Solana/Base/BNB leaders separately
via the new placeholders. faq Q1 answers with a per-chain breakdown
instead of one biased winner. seo_intro inlines the per-chain leaders
so the SERP snippet reflects reality.
* feat(core): per-chain best computation + chain-aware placeholders
Threads bestPerChain / worstPerChain through the bench-template
resolver via new {{best_name:chain:X}}, {{best_p50:chain:X}},
{{worst_name:chain:X}}, {{worst_p50:chain:X}} placeholders. The
chain-scoped regex runs before the unfiltered TEMPLATE_RE so a typo
or missing stash leaves the placeholder visible (signal rather than
silent wrong substitution).
Adds 5 happy-path tests covering the new placeholder forms + the
'leave untouched when stash absent' path.
* rpc-capabilities: declare region as top-level dimension for full-page filtering
* fix: persist bestPerChain in KV snapshot + bump cache key
PR #179 added per-chain leader stash (Benchmark.bestPerChain / worstPerChain) but missed two paths:
1) snapshotFromBenchmark only persisted results/extras/sampleSize/lastRunAt — so the KV cold-start fallback reconstructed Benchmarks without those fields, leaving {{best_name:chain:X}} placeholders raw in findings/seo_intro/faq.
2) The bench-unfiltered unstable_cache key was not bumped (still v2), so cached objects from the pre-v3 deploy could surface without the new fields.
Bump SnapshotSchema to _v:2 (writes carry bestPerChain/worstPerChain; reads expose them) and bump the unfiltered cache key to bench-unfiltered-v3 to force a fresh recomputation across the deploy. Followed both ends of the contract: write + read.
* fix: compute bestPerChain for filtered variants too
PR #181 fixed the snapshot + cache key roundtrip. Fixed bestPerChain on the unfiltered hub, but filtered variants still surfaced raw {{best_name:chain:X}} placeholders.
Root cause: bench page (src/app/benchmarks/[slug]/page.tsx:162) pre-fetches 9 variants (3 chains × 3 regions) via getBenchmark(slug, filters) → loadBenchmarkFiltered → specToBenchmark(spec, activeLabels) with isFiltered=true. A1's per-chain compute guard `!isFiltered` skipped the fan-out for those variants, so their findings/seo_intro/faq ended up in the streamed RSC payload with raw placeholders. Switching tabs client-side surfaced them.
Drop the guard. Filtered variants now compute bestPerChain too. Cost: extra 3 Prom roundtrips per filtered variant, but each variant is cached per (slug, filterSig) via loadBenchmarkFiltered's unstable_cache so the cost is paid once per cache window, not per request.
* fix(products): use relative path for badge preview img
CSP img-src 'self' on staging Preview URLs blocks the badge <img> when its src is the prod URL (SITE.url = https://openchainbench.com), surfacing the browser's broken-image glyph on every non-prod origin. Use a relative path for the in-page preview so it always loads under the current origin; keep the absolute URL in the copy-paste HTML since embedders need it to work cross-origin.
* fix: per-chain chips only for providers with real data on that chain
GMGN on /products/gmgn was showing '#2 on Base · #2 on BNB · #2 on Solana' even though GMGN only has Solana data. The chip ranks were derived from the aggregate liveSorted list, so chain-restricted providers got a phantom slot on every chain in the bench's dimensions.
- New Benchmark.providersPerChain field: set of provider slugs that returned live data per chain, computed in spec.ts during the per-chain fan-out (uses the data we were already throwing away).
- rankPerChainForBench restricts the per-chain rank set to providersPerChain[chain] when present, so a Solana-only provider only earns a Solana chip.
- Snapshot v3 + cache key bumped to bench-unfiltered-v4 so the new field survives cold-start KV restores and stale-cache invalidates cleanly across deploys.
* fix(providers): scope rankPerChainForBench to providersPerChain set
The companion change to the previous commit — restricts per-chain rank computation to providers that actually have data on that chain. Without this, a Solana-only provider's aggregate rank leaked into Base/BNB chips on /products/[slug].
* fix: bump outer all-benchmarks cache key to v6
PR #184 added providersPerChain to Benchmark + bumped the inner bench-unfiltered cache key to v4, but missed the OUTER loadAllBenchmarks cache (`all-benchmarks-v5`). The aggregate cache can keep serving v5-era Benchmark slices (no providersPerChain) even after the per-bench cache is fresh, leaving /products/[slug] showing phantom chips for chain-restricted providers.
* timeseries: click-to-exclude with smooth Y-axis re-zoom
* timeseries: drag-to-zoom with smooth Y rescale + reset button
* perf: inherit editorial copy on filtered variants instead of re-computing
PR #182 made every filtered variant run the per-chain Prom fan-out so {{best_name:chain:X}} could resolve in the variant's findings/faq/seoIntro. That quadrupled cold-cache Prom load per page (3 extra queries × 9 pre-fetched variants on benches like aggregator-head-lag), making cold-start visibly sluggish on Vercel preview URLs and pushing some bench loads close to the function timeout.
Cleaner: editorial copy is the same on every tab (it's the same YAML), so filtered variants don't need their own per-chain compute. They inherit findings / faq / seoIntro / abstract / methodology / perChainExplainer / bestPerChain / worstPerChain from the aggregate at the page-level fetch. Per-chain compute stays on the unfiltered hub only.
Net effect: page cold-start drops from 36 query sets to 9, while keeping all the chain-aware placeholders correctly resolved on every tab.
* timeseries: hide hover dots and tooltip rows for excluded providers
* fix(ledger): move chain coverage chips to second row so titles fit
Reported visually: chips ('#1 · BASE', '#1 · BNB CHAIN', '#1 · SOLANA') were rendered inline in the same flex row as the provider logo + name + tag + type badge, eating horizontal space on the name column. Names truncated to 'Mo...', 'Cod...', 'GeckoTerm...' on the bench leaderboard.
Restructure: wrap in a flex-col, keep logo/name/tag/type on top line, push the chips to a second flex-wrap row indented 28 px to align under the name. Chips still md+ only.
* timeseries: per-line hover index + gap-aware dots and tooltip rows
* fix(ledger): only render #1 chips on the bench leaderboard row
Reported visually: on wallet-labels-coverage (11 chain dimensions), every row was showing all 11 chain chips (Ethereum, Solana, BNB Chain, Base, Arbitrum, Polygon, Optimism, TON, Stellar, XRP, Bitcoin) — most rendered as neutral 'not leading on this chain' chips. Total noise: ~44 chips across 4 rows.
Default ChainCoverageChip to leaders-only mode. Non-leader chips are hard-skipped. The bench page chain tabs already provide precise per-chain rankings; this chip is a quick scan aid for 'where does this provider currently top the field' — that signal disappears when buried under 9 neutral chips per row.
leadersOnly=false escape hatch kept so /products/[slug] (or any future consumer) can opt back into the full coverage view.
* retire Leaderboard view from bench pages, default count/usd to Distribution
* Revert "Merge pull request #193 from ChainBench/feat/retire-leaderboard-view"
This reverts commit 9eeb33c601d1e73bf9582d397c82c6b6fad4602f, reversing
changes made to a5e2efb4137d332a9256f1a88454da6b9ef9edb1.
* distribution + ranked-bar: stop clamping sub-1 values to position 0 on log scale
* distribution: position p50 dot at p50Pct% (was stuck at left:0)
* distribution: redesign as percentile whisker with landmark axis
* distribution: add mean marker (outlined ring) to whisker
* audit: refresh MCP allowlist + fix bridge-revenue 404 source links + llms.txt count
* fix(bench): wrap GitHub source link on mobile
Reported visually: the 'Source code github.com/.../tree/main/harnesses/<slug>' link at the bottom of bench pages overflows the viewport on mobile (uppercase + 0.16em tracking inflates an already-80-char URL well past the screen width). Adds break-all on the <p> and max-w-full on the inline <a> so the URL wraps at any character without pushing the page horizontally.
* site-header: interactive 3D logo sphere
* Revert "Merge pull request #202 from ChainBench/feat/3d-site-logo"
This reverts commit bbf8afa96fe9cbccf794f18a99f6dccb2e88b2d0, reversing
changes made to 97754cc4836f129afb9eaeb74bf2f6d5195025f2.
* chore: remove GMGN from bench-001 + assets (#200)
* chore: remove GMGN from bench-001 + assets
Per maintainer's request — GMGN integration is being walked back.
- benchmarks/aggregator-head-lag.yml: drop the gmgn provider entry, revert methodology bullets + seo_intro mention to the pre-GMGN copy. Per-chain editorial placeholders kept (still useful for the bench).
- src/data/provider-registry.ts: drop gmgn entry.
- src/lib/logo-manifest.ts: drop gmgn entry.
- public/logos/gmgn.svg: deleted.
- harnesses/aggregator-head-lag: drop gmgn_monitor.go, GMGN_ENABLED config, GMGN startup banner + goroutine, chromium Dockerfile install, utls dep.
Per-chain architecture (bestPerChain placeholders, products page chips, badge ?chain= param, sitemap per-chain entries) is left in place — it serves metadata-coverage / rpc-capabilities / bridge benches independently of GMGN.
* chore: drop unrelated files accidentally included in prior commit
Worktree leftovers (.claude/worktrees/), local logo-3d experiment, and dev-branch site-header edits got swept in during the GMGN removal commit. Restore them out of this branch so the PR diff is GMGN-only.
* feat(bench-029): add Solana DEX quote latency spec (#203)
New bench measuring wallclock latency of Solana DEX quote APIs (Jupiter, Mobula, OpenOcean, Raydium) every 60s from 3 regions. DFlow added when partnership API key is provided.
Status starts as 'draft' — flip to 'live' after 24h of metric data accumulates in Prom.
Pairs with mobula-monorepo#6608 (harness) + #6609 (Grafana dashboard). Metric names line up with what the harness emits (solana_quote_latency_ms histogram + solana_quote_success gauge + 3 error counters). Methodology + FAQ disclose Mobula co-location bias explicitly.
* chore(harnesses): port solana-quote-latency to public repo + polish seo (#205)
Source code for bench #029 (Solana DEX quote latency) was previously
private in mobula-api/miniapps/. Move it here so the public source link
on the bench page resolves to runnable code.
Also tighten the bench seo_title/description: drop the trailing 'ms'
suffix and add the 2026 year tag, matching the pattern of the more
recent benches (solana-tx-landing-latency, perp-fees, etc.).
* chore: update repo refs from OpenChainBench/OpenChainBench to ChainBench/OpenChainBench (#206)
GitHub auto-redirects the old URL but updating in-place for cleanliness,
SEO, and to keep clone instructions valid. Covers all 19 bench YAMLs,
contributor docs, src/ links, and .github templates.
* bench-029: flip status draft -> live (Prom has samples on all 4 providers across 3 regions) (#207)
* feat(products): wire raydium + openocean logos and registry entries (#208)
Logos added by user at repo root, moved to public/logos/ and wired into
logo-manifest. Provider registry entries placed next to jupiter (Solana
DEX cluster) for bench-029 (solana-dex-quote-latency).
* bench-029: add region dimension + correct hosting claims (#209)
* feat(products): wire raydium + openocean logos and registry entries
Logos added by user at repo root, moved to public/logos/ and wired into
logo-manifest. Provider registry entries placed next to jupiter (Solana
DEX cluster) for bench-029 (solana-dex-quote-latency).
* bench-029: add region dimension + correct unverified hosting claims
- Add dimensions.region block so the page renders region tabs (us-east,
eu-west, sgp) at the top with full-page re-fetch on click, mirroring
the rpc-capabilities pattern.
- Fix unverified 'Mobula on Railway us-east' claim: DNS shows all 4
providers are CDN-fronted (Cloudflare for 3, CloudFront for Jupiter),
so the latency gap is edge-caching policy, not hosting region. Update
the methodology disclosure, the per-provider finding, and the FAQ
entry to reflect what the data actually shows.
* bench-029: warm-path harness + remove false 'Mobula operates' disclosure (#210)
* feat(products): wire raydium + openocean logos and registry entries
Logos added by user at repo root, moved to public/logos/ and wired into
logo-manifest. Provider registry entries placed next to jupiter (Solana
DEX cluster) for bench-029 (solana-dex-quote-latency).
* bench-029: add region dimension + correct unverified hosting claims
- Add dimensions.region block so the page renders region tabs (us-east,
eu-west, sgp) at the top with full-page re-fetch on click, mirroring
the rpc-capabilities pattern.
- Fix unverified 'Mobula on Railway us-east' claim: DNS shows all 4
providers are CDN-fronted (Cloudflare for 3, CloudFront for Jupiter),
so the latency gap is edge-caching policy, not hosting region. Update
the methodology disclosure, the per-provider finding, and the FAQ
entry to reflect what the data actually shows.
* bench-029: warm-path harness + remove false 'Mobula operates' claim
Two coupled changes:
1. Harness switches from cold-path (KeepAlive: -1, DisableKeepAlives:
true, new http.Client per tick) to warm-path: one persistent
http.Client per provider, TCP+TLS reused across the 60s tick, HTTP/2
attempted. The measured number is now steady-state RTT — what a
long-running backend integration actually sees — not the first-call
cold handshake penalty. Shared helper in cmd/monitor/http_client.go.
2. Methodology + FAQ updated to reflect warm-path. Drop the inaccurate
'Mobula operates OpenChainBench' disclosure (Mobula sponsors harnesses
that it hosts on its Railway infra, but does NOT operate or own the
project). Keep the CDN disclosure since DNS verification confirmed
all 4 providers sit behind a CDN — that fact still affects how the
numbers should be read.
* alternatives: pad JSON-LD description to Schema.org Dataset 50-char minimum
* fix: bust bench cache keys to surface bench-029 (#212)
The all-benchmarks-v6 and bench-unfiltered-v4 entries cached before
bench-029 (solana-dex-quote-latency) was merged were still serving from
Vercel Data Cache, so the new bench was 404 on direct URL and absent
from /benchmarks search. Bumping to v5 / v7 forces a fresh build.
* fix(brand): distinct provider colors + sgp region glyph (#213)
- Jupiter switched from #FF7A00 (amber-orange) to #C7F284 (jupiter matrix
green, their secondary brand) so it doesn't collide with mobula's
vivid orange on the solana-dex-quote-latency by-region chart.
- Add raydium (#00C2FF cyan) and openocean (#2563EB deep blue) so they
stop falling through to the editorial palette and reading as similar
orange/violet on the chart.
- Add sgp brand color + isRegion entry alias to ap-southeast. The bench
dimension value is the raw Railway label sgp, but the brand lookup
was only registered under ap-southeast, so the SGP region tab had no
globe glyph and no colored ring (rpc-capabilities had the same issue).
- Rename the SGP region label from 'AP-Southeast' to 'Singapore' on
both bench-029 and rpc-capabilities. AP-Southeast is AWS naming; for
end users Singapore is clearer.
* fix(bench): hide chart's internal region tabs when bench declares dimensions.region (#214)
When a bench YAML has dimensions.region, the top-level Region row already
controls region scoping. The TimeSeriesChart was still rendering its own
internal region tabs because the page only forwarded the region prop when
showChartRegionRow was true (the chart-only fallback path). Now we pass
the top-level region selection down whenever regionOptions exist, so the
chart hides its tabs and matches what the page-level filter selects.
Also rename the AP-Southeast chart-tab label to 'Singapore' to match the
top-level row, and add an alias for the raw 'sgp' region key.
* site-header: 3D logo on desktop, SVG on touch (#215)
* mobile: reliable sticky header + soften dot pattern (#216)
* feat: hyperliquid frontends quality bench № 030 (draft, SQLite cohort retention)
* hl-frontends: COALESCE null retention sum + METRICS_ADDR local-dev override
* mobile: fixed header + dynamic spacer, dots more visible
* bench-029 v2: rotate USDC → long-tail trending tokens (#220)
The anchored SOL → USDC pair Jupiter was serving from CloudFront edge
cache in 35 ms — that measured cache, not routing. Rotating the tokenOut
every tick from Solana volume ranks 6-55 (stables excluded) defeats any
per-pair edge cache and forces every provider to actually search a path.
Harness changes (mirrored to OCB public repo from mobula-api PR #6613):
- trending.go (new): fetches the rotation list every 10 min, picks one
token at random per tick.
- Provider contract adds tokenOut parameter; all 4 adapters refactored
to quote USDC → tokenOut at 100 USDC notional, 1% slippage.
- New counter solana_quote_no_route_total excluded from the latency
histogram (liquidity gap is not a latency outlier).
- isNoRouteSignal matches Jupiter NO_ROUTES_FOUND / TOKEN_NOT_TRADABLE,
Mobula 'No route found' / 'Token not found', Raydium INSUFFICIENT_LIQUIDITY
/ ROUTE_NOT_FOUND. OpenOcean is payload-detected (outAmount==0, dexId<0,
path==null, price_impact==-100%) since it has no string error.
- log_buffer ring is now O(1) instead of O(n) slice-shift.
YAML changes:
- methodology rewritten around the rotation, no-route counter, and
warm-path keep-alive measurement.
- per-provider formulas tightened to the verified response field name
+ the no-route handling.
- FAQ adds entries for the rotation rationale, the no-route counter, and
Raydium's single-venue scope.
* mobile: opaque bg + GPU hint on fixed header (kill notch see-through) (#221)
* hyperliquid-frontends: flip status to live (page renders, awaits real builders.json addresses for data)
* site-logo-switcher: layer SVG under 3D to kill the swap flash
* hl-frontends: shorten disclaimer + 2 methodology bullets to satisfy Zod 500-char cap
* logo-switcher: never paint SVG on desktop (CSS-gate)
* bench-029 v3: switch rotation source from REST volume-ranks to Pulse V2 WS
Sacha's ask was 'pulse trending tokens', not 'top by 24h volume'. v2's
REST volume-ranks pick was a related-but-different signal — established
mid-tier tokens with cumulative volume, not what's pumping right now.
v3 actually wires Pulse V2 in.
Harness changes (mirrored from mobula-api PR):
- New pulse_subscriber.go opens a persistent ws://pulse-v2-api.mobula.io
session, subscribes to the bonded view on Solana, dedupes by mint into
a 30-min sliding pool keyed by lastSeen. Reconnect with 5s -> 60s
exponential backoff. ~50-300 active mints in steady state.
- trending.go now exposes Note() / Forget() (called from the WS handler)
and a Pick() that prefers the pulse pool over the REST snapshot. REST
refresh kept on a 10-min cron as the fallback for Pulse outages.
- scheduler.go boots both goroutines, waits up to 30s for either source
to fill the pool, then ticks. The 'src=PULSE|REST' log per tick makes
the active source observable in real time.
- gorilla/websocket added to go.mod.
YAML methodology rewritten to describe the Pulse rotation. Raydium's
no-route rate disclosure bumped from ~30-50% to ~80% because the
bonded view is mostly Pump.fun graduates living on PumpSwap, not on
Raydium-native pools.
Local validation: 6 ticks observed, 5/6 sourced from PULSE, 6 distinct
tokens (FARTCOIN, tolytown, cbBTC, Electricia, DUCKY, TOPLESS). Jupiter
no-routes on 1, OpenOcean on 1 (Electricia), Raydium on 5 (as expected).
Pool grew 50 -> 61 over the run.
* hl-frontends: real builder addresses (Phantom, Axiom, pvp.trade, Insilico, Defiapp, MetaMask, Dexari, Okto)
* logo-switcher: instant sphere placeholder on desktop while 3D loads
* add logos + registry entries for 8 hyperliquid frontends
* logo: static import 3D, drop dynamic + placeholder
* hl-frontends: registry entries + logo manifest mappings (dropped from earlier squash)
* fix(l1-finality): switch Litecoin from Blockchair to litecoinspace.org (#233)
Blockchair's free-tier IP-blacklisted Railway's egress; the Litecoin
fetcher had been returning HTTP 430 'temporary blacklisted' on every
tick (359 errors/h, l1_finality_health{chain=litecoin}=0, the
last_refresh_timestamp gauge was never even emitted because no fetch
ever succeeded).
Switched to litecoinspace.org (mempool.space-style Esplora API for
Litecoin) — free, no API key required, no IP blacklist. The new fetcher
is one call per tick (GET /api/blocks returns 15 recent blocks with
height + unix timestamp; the 12-confirmations finalized block is
index 12 of the array) vs Blockchair's two-call /stats + /dashboards
flow. Fallback to a second /api/blocks/<height> call if the confirmation
depth ever exceeds the default window.
Verified locally: lag=1912s (= 32 min, matches 12 confirmations × 2.5
min LTC block time), fetch latency 33ms warm-path.
* hl-frontends: switch headline from rolling 24h to last complete UTC day
* fix: mobile overflow on benchmark detail pages
- Tables (ledger, region-grid): min-w only applies from sm: so mobile
shrinks the table inside its overflow-x-auto wrapper instead of
forcing 480/520px and pushing the whole page past the viewport edge
- count-leaderboard summary strip stacks 1-col on mobile, reduces
gap-x so 'RANGE 40 → 306 7 providers' no longer cuts off
- summary-stat removes the 8rem min-width on mobile flex children
- breadcrumb + bench identifier lower tracking on mobile, last crumb
truncates at 60vw so long titles can't push the row off-screen
- Title + subtitle get break-words as safety net
- html { overflow-x: clip } as a final root-level safety net
* fix: site-logo mask id collides between hidden header + visible footer
useId() gives each instance its own mask id. The shared 'c-mask-header'
id was being defined by both the header (display:none on desktop) and
the footer; Safari ignores a mask whose owning SVG is hidden, so the
footer painted an unmasked filled circle instead of the C cutout.
* hl-frontends: walk back 3 days to handle Hyperliquid bucket publish lag
* fix(l1-finality): slow Cardano polling to 60s to stay under Koios free-tier quota
Cardano was DOWN for ~96h: l1_finality_health{chain=cardano}=0, 359 errors/h
via api.koios.rest. Koios responds 200 from any clean IP, but the harness on
Railway hits 429/403 — Koios's free tier is 5,000 req/day/IP, and our default
10s cadence × 2 calls per tick = 17,280 req/day, which burns the quota in
~7h then blacklists the egress IP.
Add a per-chain MinIntervalSeconds field to ChainConfig: when > 0, the
fetchAll dispatcher skips ticks for that chain until enough time has passed.
Cardano set to 60s → 2,880 req/day, safely under the quota. Other chains
keep the global 10s cadence (Solana/Tron/EVM via direct RPC have no such
quota).
Resolution loss is minimal: Cardano blocks are ~20s and 15 confirmations
takes 5 min of wall-clock to finalize, so per-minute polling is plenty.
Verified locally: 2 Cardano ticks in 75s (~60s cadence), lag=478-513s
(= 15 confs × ~20s + empty-slot variance), other chains still 8 ticks in 75s.
* fix(l2-block-time): head-watchdog to catch ghost WS connections
Scroll row went silent: samples_total stuck at 23,887 for >1h, 0
reconnects in the same period, health=1. The reconnect loop never
fires because the pong handler bumps SetReadDeadline on every pong,
so a provider that keeps the WS heartbeat alive while silently
dropping our newHeads subscription wedges the read loop forever.
Scroll already had 109 lifetime reconnects (vs 12-37 on the other 8
L2s), confirming the publicnode → Scroll WS link is the flakiest of
the bunch.
Add a per-connection head-watchdog goroutine: stores the timestamp
of the last received newHead in an atomic int64, checks every 15s,
and forces a conn.Close() if the silence exceeds 90s. Closing the
conn makes ReadMessage return → runChain returns → outer reconnect
loop bumps the counter and dials again. All 9 L2s have block-time
under 15s, so 90s of silence is unambiguously a dead subscription.
Verified locally: 6 Scroll blocks in 30s plus all 8 other L2s
emitting at their natural cadences; watchdog stays quiet under
normal traffic.
* timeseries: interpolate hover-dot Y between sample brackets so it sits on the line
The hover dot was using Math.round to snap to the nearest sample's value,
which disagreed visually with the line during steep slopes. The line is
drawn as straight SVG segments between samples (M…L…L…), so at hover-X
between sample N and N+1 the line is at lerp(v[N], v[N+1], t) — but the
dot was at either v[N] or v[N+1] depending on which way Math.round broke.
Most visible on bench-029 when a provider's quote goes from 1.5s -> 200ms
between two consecutive samples: dot floats above or below the descending
segment.
Fix: floor instead of round to get the bracketing pair, lerp the value at
the fractional position. Dot now lands exactly on the rendered line. The
skip-rendering rule is preserved (no dot when either bracket is a gap).
* fix: stronger mobile overflow safety net for Safari + Telegram WebView
- overflow-x: clip on html AND body (WKWebView ignores it on html alone)
- max-width: 100vw on body to harden against any wide grandchild
- break-words on SEO intro + methodology paragraphs
* fix: shrink region-grid + hide extra ledger cols on mobile
aggregator-head-lag (and other geo-distributed Solana benches) showed
overflow on iPhone because:
- region-grid cells used grid-cols-[1fr_5rem] on every viewport; with
3 regions + a 128px Product column the table intrinsic min was ~404px
> 375px iPhone SE viewport. Now: grid-cols-[1fr_auto] on mobile,
Product col w-20 (80px) instead of w-32, tighter gap, smaller value
font. Stays at the original densities from sm: up.
- ledger Slot delta and Secondary columns were visible on mobile (no
hidden md:table-cell), adding 2 extra mono columns to benches with
hasSlots/secondary. Now hidden on mobile like the other aggregate
columns (p90/p99/Mean/Delta/Success).
* fix: defensive overflow-x: clip on main + article (mobile WebView)
The html/body-level clip from PR #242 is sometimes ignored by iOS WKWebView
(Telegram in-app browser, certain Safari versions) when a deep descendant
forces width > viewport. Adding the clip on main and article means the
guard applies on a non-root box where browsers honour it consistently.
Also w-full + min-w-0 + max-w-full at each level so flex/grid children can
actually shrink instead of inheriting an intrinsic min-content width.
LiveIndicator: drop the 'updated Ns ago' suffix on <sm screens. The
inline-flex container can't wrap internally, so on a 375 px viewport the
whole bench-identifier row was pushed beyond the edge. Dot + Live keeps
the freshness signal without the width tax.
* fix(solana-quote): align region key to sgp so SGP tab renders (#245)
* chore: remove em-dashes / en-dashes from user-facing content (#246)
User wants prose to read less AI-generated. Replaced all em-dashes (—)
and en-dashes (–) in rendered content with commas, periods, colons or
parentheses depending on context. Comment-only dashes (JSDoc, //, /* */)
kept as-is since they never reach the rendered site.
Files touched: 5 YAML benchmark specs, 1 markdown bench page, provider
registry descriptions, OG / Twitter image alt text, alternatives SEO
description, MCP API param description, donut-chart placeholder.
Mobula affiliation audit: clean. The site already reads as a neutral
third-party benchmark project. Mobula appears only as a benchmarked
provider alongside Codex, GeckoTerminal, etc., never as parent / owner.
* feat(hl-frontends): cutover to v2 (sub-minute freshness from local hl-node) (#247)
Swap p50/p99/sample_size/series/mean queries to the *_v2 metrics emitted
by the new hyperliquid-frontends-local harness on OVH SGP. Volume/fees/
effective_fee_bps/fills/fees_per_user now refresh every 30s instead of
the 24-48h bucket lag.
Keep p90 = hl_frontend_d7_retention_pct{} from v1 since v2 doesn't have
SQLite retention state yet (deferred to v2.1).
Replace the success metric (csv_fetch_status was specific to the bucket
fetcher; v2 has no HTTP layer) with a harness-liveness heartbeat:
hl_frontend_local_last_tick_unix_v2 must be < 120s old.
Note: the v2 rolling 24h window started filling at 14:21 UTC today, so
volumes will look low for the next ~22h until the window is fully
populated. After that they'll match v1's full-day numbers.
* feat(hl-frontends): refresh findings + FAQ with v2 bangers (deviation, outage, taker, per-asset) (#248)
Replace the old findings block (template-based, generic) with 6 new
findings that cite specific live numbers from the v2 metrics:
- effective fee spread 10x across cohort (Insilico 1.00 → MetaMask 10.00)
- price deviation as user-cost signal (Dexari 25.83 bps, 13x median)
- volume ≠ user-friendliness (Phantom #1 volume but Insilico cheapest)
- two live outage signals (Dexari 19min stale, Okto 24h inactive)
- taker share separates aggressive UIs from pro terminals
- asset concentration reveals routing strategy (HIP-3 stocks on Dexari/MM)
Plus 2 new FAQ entries explaining what price_deviation and last_fill_age
actually measure, since they are unique to this bench in the HL space.
* feat(bench): switchable companion metric panels under main ledger (#249)
Spec authors can now declare any number of `metric_panels` (up to 8) in
a bench YAML. Each panel names one Prometheus metric; the loader queries
it per provider as `<metric>{<label_key>="<slug>"}` and stores the
scalar values. A new MetricPanelGrid component renders below the main
table with a tab row at the top; clicking a tab re-ranks the cohort by
that panel's metric, applying its higher_is_better direction.
Hyperliquid frontends ships first with 5 panels: slippage proxy, time
since last fill (outage), fills per minute (activity), taker share, and
volume routed. All values come from the local hl-node v2 harness at
sub-minute freshness.
Non-breaking: benches without `metric_panels` render exactly as before.
Cardinality impact on Prom is bounded (5 metrics x 8 providers = 40 new
series across all panels for this bench).
* feat(bench): metric view tabs swap the main chart instead of stacking tables (#250)
Previous PR rendered each metric panel as a separate static table under
the main chart. This commit replaces that with a tab row above the chart
itself: clicking a tab swaps which metric the chart displays (per-builder
multi-line, 24h trailing) so the reader stays in a single visual context
and can compare trends instead of snapshots.
Implementation:
- spec.ts now fetches a 24h series for each panel x provider alongside
the scalar values (`prom.series(query, winSec, 72)`).
- MetricPanel gains an optional `seriesByProvider` field.
- TimeSeriesChart accepts new optional props (seriesOverride,
metricLabelOverride, unitOverride) that swap its data source + header.
- New MetricViewTabs component pure presentation; parent owns active id.
- benchmark-body wires the state, panel description rendered under chart.
- Drops the old MetricPanelGrid table component (no longer used).
Non breaking: benches without `metric_panels` render exactly as before
(the tab row is conditionally rendered only when panels are declared).
* fix(chart): include metric panel id in seriesKey so chart remounts on tab swap (#251)
The Chart child component used `key={seriesKey}` where seriesKey only
encoded range + region. When the parent swapped seriesOverride via a
metric panel tab click, lines useMemo did recompute (override added to
deps) but the Chart child's internal state (drawn paths, slicedLines)
kept the prior render cached. Adding the active panel label to the key
forces a clean remount on tab swap.
* docs(hl-frontends): refresh disclaimer, seo_intro, abstract, methodology, faq for v2 pipeline (#252)
Rewrites the bench documentation to describe the local hl-node fills
stream rather than the deprecated daily CSV bucket. The methodology
list now covers all 9 metrics actually emitted (effective fee, fees
per user, price deviation, last fill age, fills per minute, taker
share, per asset top three, plus the heartbeat and rolling window
mechanics). FAQ entries that referenced the CSV URL or the old
frontend list (OKX, GMGN, Dexterity, KGEN, ASXN) updated to match
the current builders.json (Phantom, Axiom, pvp.trade, Insilico,
DefiApp, MetaMask, Dexari, Okto). Source URL points at the new
miniapps/hyperliquid-frontends-local harness.
No em dashes anywhere in prose per content guideline. All character
caps respected.
* chore: safe cleanup — dead code, stale lint suppressions, unused vars (#253)
Audit-driven cleanup verified to have zero visible impact on the site.
Deletes (confirmed zero imports across src/):
- src/components/benchmark-table.tsx (201 LOC, never imported)
- src/lib/methodology-tooltip.ts (37 LOC, helper never wired up)
- src/lib/utils.ts (cn() helper, never called)
- CURRENT_ISSUE and ISSUE_DATE_LONG exports in site.ts
Lint cleanups:
- Drop three stale eslint-disable directives (apple-icon, icon,
benchmarks/[slug]/page) for rules that no longer fire
- Fix two unescaped apostrophes in mcp/page.tsx
- Remove unused activeTemplate variable in share-section.tsx
- Drop padB / padR from a useMemo deps array in time-series-chart.tsx
where they were never read in the closure
Kept on purpose despite audit suggestions:
- .card CSS class (used 5x by card / live / benchmark-grid / body)
- --color-accent-soft and --color-banner-* tokens (used by Tailwind
utilities in benchmark-grid, view-switcher, home-bench-table,
site-banner)
- GithubIcon custom SVG (lucide-react dropped its GitHub brand mark)
- Static three.js import on the 3D logo (replacing with next/dynamic
re-introduces the SVG flash that PR #231 deliberately removed)
- Hook-rule and ref-during-render lints in benchmark-body /
time-series-chart: real refactors, out of scope for a zero-impact
cleanup, deferred to a follow-up
Verified: pnpm typecheck clean, pnpm build green.
* hl-frontends: expand providers 8 → 60 (DefiLlama registry sync) (#254)
* chore: remove SiteBanner (#256)
User reports the banner leaves a phantom gap in Telegram WebView even
once dismissed (localStorage doesn't always persist in in-app browsers,
and the wrapper's padding still measures even when the banner returns
null). Drop the component, drop the import, drop the spacer's banner-
related comment + the now-irrelevant initial height estimate.
* chart: add Top 5/10/20/All selector when cohort > 10 providers (#257)
* chart: size top-N selector off registered cohort, not emitted series (#258)
* chore: delete verified dead code (real pass, not agent guesses) (#255)
This time I greped every file myself instead of trusting agents.
Deletes (zero usages confirmed across src/):
- src/components/live-dashboard.tsx — 6-line re-export shim; inlined the
import in app/page.tsx so the shim has nothing left to redirect.
- src/lib/brand.ts: brandIsDark() — never called anywhere.
- src/lib/live/format.ts: fmtAge() — never called. live-indicator.tsx
has its own formatAge() with day handling that fmtAge lacks, so they
weren't an actual duplicate either.
- src/components/chain-coverage-chip.tsx: leaderChains(),
perChainRankSegments(), chainLabelFor(), chainLeader() — exported but
never imported AND never called from within the same file. TypeScript
noUnusedLocals confirms they were stranded.
Visibility tightened:
- src/lib/brand.ts: isLight() now non-exported (only used by
chipTextColor in the same file).
Drive-by cleanup that fell out:
- Dropped ProviderResult import in chain-coverage-chip.tsx (only the
deleted helpers used it).
Verified clean: pnpm typecheck + pnpm tsc --noEmit --noUnusedLocals
--noUnusedParameters + pnpm build all pass. Net: -79 lines.
* fix: drop overflow-x clip from body so position: fixed sticks on WKWebView (#259)
User reports a 1-2 px gap at the top of the header in Telegram in-app
Safari that lets page content peek through during scroll. Root cause:
body had overflow-x: clip (PR #242 safety net) and WebKit treats clip
on body as creating a containing block for position: fixed descendants.
The fixed header was then anchored to body's top edge, not the viewport,
and momentum scroll desynced it from the URL-bar animation, producing
the visible slit.
Move the overflow-x clip safety net to <html> only. <main> and <article>
already carry overflow-x-clip (PR #244) so page-level horizontal overflow
remains contained without needing it on body. Drop the translate3d hint
on the header — it was paired with the clip workaround and is not needed
once fixed positions cleanly against the viewport.
* hl-frontends: 60-builder registry, real logos, methodology refresh, availability backstop (#260)
* hotfix(hl-frontends): trim methodology line 8 to <=500 chars (Zod limit) (#261)
* fix: switch header to position: sticky (iOS 26 WebKit fixed jitter bug) (#262)
Web search confirmed user is not alone. WebKit bug 297779 reports that
on iOS 26 Safari + WKWebView (Telegram in-app browser, Chrome iOS), any
position: fixed element drifts 10-24 px when scroll direction reverses,
exposing the page under the header through a thin gap. Mastodon and
LinkedIn ship the same symptom. Apple acknowledged the bug, partially
fixed it in iOS 26.1 but residual reports persist into Dec 2025.
position: sticky uses a different render path in WebKit that does not
trip the jitter heuristic. It is also simpler:
- no spacer div needed (the element stays in normal flow)
- no useRef + ResizeObserver to keep the spacer in sync
- no headerH state
Net: -25 lines on top of fixing the cosmetic gap. Desktop and standalone
Safari behavior are identical, sticky just renders more reliably in
WebViews.
Refs:
- https://bugs.webkit.org/show_bug.cgi?id=297779
- https://github.com/mastodon/mastodon/issues/36144
- https://github.com/TelegramMessenger/Telegram-iOS/issues/1748
* fix: drop rows with zero headline value from ledger and ranked-bar (#264)
* fix: switch header to position: sticky (iOS 26 WebKit fixed jitter bug)
Web search confirmed user is not alone. WebKit bug 297779 reports that
on iOS 26 Safari + WKWebView (Telegram in-app browser, Chrome iOS), any
position: fixed element drifts 10-24 px when scroll direction reverses,
exposing the page under the header through a thin gap. Mastodon and
LinkedIn ship the same symptom. Apple acknowledged the bug, partially
fixed it in iOS 26.1 but residual reports persist into Dec 2025.
position: sticky uses a different render path in WebKit that does not
trip the jitter heuristic. It is also simpler:
- no spacer div needed (the element stays in normal flow)
- no useRef + ResizeObserver to keep the spacer in sync
- no headerH state
Net: -25 lines on top of fixing the cosmetic gap. Desktop and standalone
Safari behavior are identical, sticky just renders more reliably in
WebViews.
Refs:
- https://bugs.webkit.org/show_bug.cgi?id=297779
- https://github.com/mastodon/mastodon/issues/36144
- https://github.com/TelegramMessenger/Telegram-iOS/issues/1748
* ranked-bar+ledger: add Top-N selector and drop unscored rows
* ledger+ranked-bar: drop rows with zero headline value across p50/p90/p99
* chart: slice seriesOverride by range so panel tabs respect 1h/6h (#265)
* chart: Top-N buttons reflect available data (hide useless options) (#266)
* fix: drop sticky on mobile (iOS 26 WebKit jitter workaround) (#267)
position: sticky and position: fixed both jitter 10-24 px on iOS 26 when
scroll direction changes (bugs.webkit.org/297779). Apple acknowledged
the bug, partial fix in 26.1, residual reports into late 2025. Mastodon
and LinkedIn ship the same symptom and concluded it is unfixable from
the site side.
Mitigation: keep sticky only on md+ where the desktop render path is
clean. On mobile the header stays in normal flow, scrolls away with the
page, and the jitter cannot appear because nothing is positioned. Same
pattern as most mobile sites — pull to top to see the nav.
Single class change: `sticky top-0` -> `relative md:sticky md:top-0`.
* fix: keep sticky header on regular mobile, only drop it in iOS WebViews (#268)
Previous PR (#267) dropped sticky on all mobile to dodge the iOS 26
WebKit jitter bug. Overkill — the symptom only manifests visibly inside
in-app WebViews (Telegram, Instagram, Facebook, Twitter, TikTok) where
the host's own URL-bar overlay animates on top of WebKit's misposition.
Regular mobile Safari and Chrome iOS render sticky cleanly enough for
end users.
Mechanism:
- Inline script in layout.tsx head detects iOS WebView via UA (no
`Safari/` token on an iOS device = WKWebView in-app host) and adds
the `ios-webview` class on <html> before React hydrates.
- globals.css scopes a non-sticky override to that class on the site
header at < md viewports.
- site-header.tsx exposes a stable identifier class (`site-header-root`)
and stays sticky-top-0 by default everywhere else.
No flash on first paint because the class lands during the same render
tick as the dark-mode detection script next to it. Build + typecheck
clean.
* fix(chart): Top-N selector on all 4 chart views (ranked, time, distribution, donut) (#269)
* feat(bench): network fees — current native transfer cost USD across 11 L1 chains
* chart: Top-N selector on all 4 chart views via shared hook+component
* ledger: follow active panel — sort/filter/value per panel metric (#271)
* wallet-labels-coverage: drop All chains aggregate tab (#272)
* feat(bench): network fees — current native transfer cost USD across 11 L1 chains
* chart: Top-N selector on all 4 chart views via shared hook+component
* wallet-labels-coverage: drop All chains aggregate tab
* feat(bench): network fees — current native transfer cost USD across 11 L1 chains (#270)
* bench-body: pass activePanel to LedgerTable + dynamic ledger title (#273)
* hotfix(bench-body): restore ledger title template literal + closing brace (#276)
* feat(network-fees): L1/L2 toggle + 9 L2 providers
Flotapponnier
added a commit
that referenced
this pull request
Jun 5, 2026
…+ accumulated improvements) (#316) * chore(obs): temporary [DRAFT-TRACE] logs on bench draft fallback paths (#147) Logs every fork that produces a visible draft render to the user: kv_hit / kv_miss / kv_empty / kv_shape / kv_stale / kv_timeout / kv_neterr / kv_http / collapse / placeholder_used Includes slug + timing per call. All prefixed with [DRAFT-TRACE] for easy grep in Vercel logs. Plain console.warn — no extra deps. Revert this commit once we have the cause pinned. * fix(harness): bump Astralane default tip 500k → 1M lamports (#148) * chore(obs): temporary [DRAFT-TRACE] logs on bench draft fallback paths Logs every fork that produces a visible draft render to the user: kv_hit / kv_miss / kv_empty / kv_shape / kv_stale / kv_timeout / kv_neterr / kv_http / collapse / placeholder_used Includes slug + timing per call. All prefixed with [DRAFT-TRACE] for easy grep in Vercel logs. Plain console.warn — no extra deps. Revert this commit once we have the cause pinned. * fix(solana-tx-landing harness): bump Astralane default tip to 1M lamports Sync from the private mobula-api harness. Astralane raised their min tip floor 500_000 → 1_000_000; keeps the public harness reproducing the same probe config. * fix(perf): bump Prom timeout 4s→10s + cron pre-warms bench cache (#149) Two root causes pinned via [DRAFT-TRACE] logs on staging: 1. Prom queries quantile_over_time(0.99, …[24h]) consistently take 4-8s under load. The 4s client timeout aborted, collapsing benches to draft when the per-bench KV snapshot wasn't available as fallback. Bump to 10s — fits the observed worst case with headroom. 2. The /api/cron/health-check route runs every 5 min but only computed Slack transitions. Add a loadAllBenchmarks() call so the per-bench unstable_cache stays warm and writeSnapshot() refreshes the KV snapshot layer. Eliminates the cold-start window where a fresh Vercel function instance has no in-memory cache AND the snapshot is stale or missing — exactly the pattern that produces the visible 'bench in draft' symptom users observed. * fix(logo): wrap 1RPC / Blocknative / Lava / Tenderly / Jito / Astralane in light chip (#150) * fix(perf): bump Prom timeout 4s→10s + cron pre-warms bench cache Two root causes pinned via [DRAFT-TRACE] logs on staging: 1. Prom queries quantile_over_time(0.99, …[24h]) consistently take 4-8s under load. The 4s client timeout aborted, collapsing benches to draft when the per-bench KV snapshot wasn't available as fallback. Bump to 10s — fits the observed worst case with headroom. 2. The /api/cron/health-check route runs every 5 min but only computed Slack transitions. Add a loadAllBenchmarks() call so the per-bench unstable_cache stays warm and writeSnapshot() refreshes the KV snapshot layer. Eliminates the cold-start window where a fresh Vercel function instance has no in-memory cache AND the snapshot is stale or missing — exactly the pattern that produces the visible 'bench in draft' symptom users observed. * fix(logo): wrap dark-toned provider logos in light chip 1RPC, Blocknative, Lava, Tenderly, Jito and Astralane have single-tone (mostly dark) artwork that disappears against the page background on small bench cards. Add to NEEDS_LIGHT_CHIP so they render on a white chip with a hairline shadow — same treatment as Relay, Debridge, Tonapi. * fix(logo): also chip dydx v4 and Sky maker (#151) Same single-tone artwork pattern as the previous batch — invisible on the bench card background. Wrap in white chip for contrast. * fix(logo): chip Merkle, Moralis, Nodies — invisible in dark mode (#152) * fix(logo): introduce NEEDS_DARK_CHIP for white-fill marks (Jito, Astralane, Sky, dydx) (#153) Verified via SVG fill audit: those 4 logos have fill="white" content, which the previous white chip rendered as invisible-on-invisible. Split into two sets — light chip (#fff for dark logos) and dark chip (#0f172a for white logos). * fix(build): tolerate all-bench-draft state instead of crashing build (#154) The safety throw 'every bench draft, refusing to update cache' crashes next build when Prom is fully unreachable during the build window. Replace the throw with a [DRAFT-TRACE] warning + return the draft array. With the snapshot/KV fallback + 10s Prom timeout + cron pre-warm already mitigating user-visible drafts, this safety net's value is marginal. * crop 1rpc/tenderly/lava logos to icon-only (#155) * replace blocknative logo with cleaner vector version (#156) * feat(bench-005): add 4 providers to network-coverage (#157) * feat(bench-005): add coinpaprika, covalent, coinstats, sim-dune to network-coverage * feat(bench-005): add coinpaprika/covalent/coinstats logos * fix(bench-005): correct CoinStats endpoint path * feat(products): add coinpaprika/covalent/coinstats/sim-dune/dune to provider registry + swap dune logo to PNG (#158) * fix(bench-005): rename sim-dune to dune + sync OSS harness mirror with 4 providers (#159) * feat(bench-027): add Mobula Swap-Send to solana-tx-landing-latency (#160) * feat(bench-027): add Mobula Swap-Send to solana-tx-landing-latency providers * feat(harnesses): sync solana-tx-landing OSS mirror with Mobula provider * fix(bench-027): trim Mobula formula to 240 chars (Zod schema limit) (#161) * fix(bench-027): rename Mobula Swap-Send to Mobula in product ledger (#162) * feat(products): consolidate alias slugs (helius-sender/avalanche-official/oracle pairs) into one canonical entry (#163) * feat(bench): simplify provider display names to brand-only across benches (#164) * fix(distribution): auto log scale when dynamic range > 50x The distribution view used a linear scale anchored at max(p99). On benches with very wide ranges (l1-finality spans 0.4s on TON to 36 min on Monero, ratio 5370x) every fast row collapsed to a single invisible pixel at the left edge, making 7 of 9 chains visually indistinguishable. Only the two slowest rows had visible bars. Port the same auto-log heuristic ranked-bar-chart already uses: when max/min > 50, switch to base-10 log projection so each row gets a proportional, distinguishable position. Footer flags 'log scale' so the reader knows when it kicks in. Same component is shared by every bench in distribution view, so this fixes l1-finality, l2-block-time, validator-yield, perp-fees and any other bench with wide dynamic range in one shot. * fix(distribution): anchor left edge at fieldMin, not absolute zero Narrow-range benches with small absolute minimums collapse every median dot into a single sliver at the left. solana-tx-landing-latency is the canonical case: 5 providers all have p50 = 2 slots on a 14-slot field, so every median lands at 14 % of the track and there is no visible spread between the leaders. Switch both linear and log scale to use fieldMin as the left baseline. The leader now sits at 0 % and laggards spread across the full track. Footer always shows 'min X' so the reader knows the chart is relative to observed range, not absolute zero. * scrub deploy IDs and team scope from public workflow * feat(bench-001): add GMGN as a provider (Solana-only) + sync harness GMGN's WebSocket pushes Solana chain-tip events (channel=chain_stat, gk='s', rs=ms:slot) sub-second. Adds it as the 4th provider on the head-lag bench. YAML: - New provider entry; queries deliberately omit chain= so per-tab filters auto-hide GMGN on Base/BNB and surface it on All/Solana. - Methodology + cardinality lines updated. Harness sync (matches private mobula-api/miniapps/aggregator-head-lag): - gmgn_monitor.go: chromedp cf_clearance mint + utls HelloChrome_120 ClientHello (h1-only ALPN) + WS subscribe + slot-event parser. - Dockerfile: chromium + minimal X/font libs for chromedp. - go.mod: utls + chromedp deps. - log_buffer.go, config.go, main.go: GMGN_PROXY env + startup banner + goroutine launch. Validated: bun run validate, typecheck, spec tests all green. * chore(harness/aggregator-head-lag): drop committed build binary + gitignore it * scrub slack webhook and grafana URL from metadata-coverage * remove alerting from metadata-coverage harness * restore alerting with env-var webhook (envsubst pattern) * feat(products): wire GMGN logo + registry entry for /products/gmgn Auto-renders /products/gmgn with brand logo, description and bench appearances now that GMGN appears in bench-001 results. Logo SVG provided by maintainer (12KB, official asset). * rename relay-link-revenue bench to bridge-revenue * add 30d range to time-series chart * drop relay-link-revenue redirect (low traffic, not worth keeping) * fix bridge-revenue seo_description over 320-char Zod limit * rpc-capabilities: per-region metric labels + multi-region YAML aggregation * rpc-capabilities: auto-detect RAILWAY_REPLICA_REGION + 3-region text in YAML * rpc-capabilities: per-region queries enable region tabs on chart * metadata-coverage: per-chain editorial (Solana/BNB leaders explicit) * bridge-fee: flag cross-corridor aggregate; defer per-corridor rewrite * bridge-quote-latency: flag cross-corridor aggregate; defer per-corridor rewrite * rpc-capabilities: per-chain editorial (Ethereum/Base/BNB/Arbitrum leaders explicit) * seo: chain-aware @id suffix in jsonld + page-metadata helpers * feat(products): add chain-coverage chip + per-chain contract bridge * seo: bench generateMetadata reads ?chain= and renders chain-honest title + description * feat(providers): chain-aware ranking + per-chain win counting * seo: emit per-chain OG + twitter cards via generateImageMetadata * feat(badge): accept ?chain= and label scope in SVG * feat(seo): chain-aware metadata + OG + sitemap + JSON-LD * feat(products page): render per-chain rank chips and clarify wins column * feat(products+badge): chain-aware ranking + per-chain badge + coverage chip * core: bestPerChain + worstPerChain stash with chain-aware helpers Adds per-chain leader/trailer computation in spec.ts that runs only on the unfiltered All view of benches with dimensions.chain. Fan out one extra tryLoadLive per chain (excluding 'all') with the chain label injected, then stash leader + trailer on the Benchmark object. Exports bestForChain (lib/spec) and leaderForChain (lib/citation) so the SEO surface + badge endpoints can pull a chain-specific winner without recomputing. * bench-001(yaml): rewrite findings/faq/seo_intro for per-chain leaders Stops surfacing a chain-mix-biased global winner on copy seen across the site. findings #1 calls out the unfiltered aggregate is biased, then per-chain bullets break out Solana/Base/BNB leaders separately via the new placeholders. faq Q1 answers with a per-chain breakdown instead of one biased winner. seo_intro inlines the per-chain leaders so the SERP snippet reflects reality. * feat(core): per-chain best computation + chain-aware placeholders Threads bestPerChain / worstPerChain through the bench-template resolver via new {{best_name:chain:X}}, {{best_p50:chain:X}}, {{worst_name:chain:X}}, {{worst_p50:chain:X}} placeholders. The chain-scoped regex runs before the unfiltered TEMPLATE_RE so a typo or missing stash leaves the placeholder visible (signal rather than silent wrong substitution). Adds 5 happy-path tests covering the new placeholder forms + the 'leave untouched when stash absent' path. * rpc-capabilities: declare region as top-level dimension for full-page filtering * fix: persist bestPerChain in KV snapshot + bump cache key PR #179 added per-chain leader stash (Benchmark.bestPerChain / worstPerChain) but missed two paths: 1) snapshotFromBenchmark only persisted results/extras/sampleSize/lastRunAt — so the KV cold-start fallback reconstructed Benchmarks without those fields, leaving {{best_name:chain:X}} placeholders raw in findings/seo_intro/faq. 2) The bench-unfiltered unstable_cache key was not bumped (still v2), so cached objects from the pre-v3 deploy could surface without the new fields. Bump SnapshotSchema to _v:2 (writes carry bestPerChain/worstPerChain; reads expose them) and bump the unfiltered cache key to bench-unfiltered-v3 to force a fresh recomputation across the deploy. Followed both ends of the contract: write + read. * fix: compute bestPerChain for filtered variants too PR #181 fixed the snapshot + cache key roundtrip. Fixed bestPerChain on the unfiltered hub, but filtered variants still surfaced raw {{best_name:chain:X}} placeholders. Root cause: bench page (src/app/benchmarks/[slug]/page.tsx:162) pre-fetches 9 variants (3 chains × 3 regions) via getBenchmark(slug, filters) → loadBenchmarkFiltered → specToBenchmark(spec, activeLabels) with isFiltered=true. A1's per-chain compute guard `!isFiltered` skipped the fan-out for those variants, so their findings/seo_intro/faq ended up in the streamed RSC payload with raw placeholders. Switching tabs client-side surfaced them. Drop the guard. Filtered variants now compute bestPerChain too. Cost: extra 3 Prom roundtrips per filtered variant, but each variant is cached per (slug, filterSig) via loadBenchmarkFiltered's unstable_cache so the cost is paid once per cache window, not per request. * fix(products): use relative path for badge preview img CSP img-src 'self' on staging Preview URLs blocks the badge <img> when its src is the prod URL (SITE.url = https://openchainbench.com), surfacing the browser's broken-image glyph on every non-prod origin. Use a relative path for the in-page preview so it always loads under the current origin; keep the absolute URL in the copy-paste HTML since embedders need it to work cross-origin. * fix: per-chain chips only for providers with real data on that chain GMGN on /products/gmgn was showing '#2 on Base · #2 on BNB · #2 on Solana' even though GMGN only has Solana data. The chip ranks were derived from the aggregate liveSorted list, so chain-restricted providers got a phantom slot on every chain in the bench's dimensions. - New Benchmark.providersPerChain field: set of provider slugs that returned live data per chain, computed in spec.ts during the per-chain fan-out (uses the data we were already throwing away). - rankPerChainForBench restricts the per-chain rank set to providersPerChain[chain] when present, so a Solana-only provider only earns a Solana chip. - Snapshot v3 + cache key bumped to bench-unfiltered-v4 so the new field survives cold-start KV restores and stale-cache invalidates cleanly across deploys. * fix(providers): scope rankPerChainForBench to providersPerChain set The companion change to the previous commit — restricts per-chain rank computation to providers that actually have data on that chain. Without this, a Solana-only provider's aggregate rank leaked into Base/BNB chips on /products/[slug]. * fix: bump outer all-benchmarks cache key to v6 PR #184 added providersPerChain to Benchmark + bumped the inner bench-unfiltered cache key to v4, but missed the OUTER loadAllBenchmarks cache (`all-benchmarks-v5`). The aggregate cache can keep serving v5-era Benchmark slices (no providersPerChain) even after the per-bench cache is fresh, leaving /products/[slug] showing phantom chips for chain-restricted providers. * timeseries: click-to-exclude with smooth Y-axis re-zoom * timeseries: drag-to-zoom with smooth Y rescale + reset button * perf: inherit editorial copy on filtered variants instead of re-computing PR #182 made every filtered variant run the per-chain Prom fan-out so {{best_name:chain:X}} could resolve in the variant's findings/faq/seoIntro. That quadrupled cold-cache Prom load per page (3 extra queries × 9 pre-fetched variants on benches like aggregator-head-lag), making cold-start visibly sluggish on Vercel preview URLs and pushing some bench loads close to the function timeout. Cleaner: editorial copy is the same on every tab (it's the same YAML), so filtered variants don't need their own per-chain compute. They inherit findings / faq / seoIntro / abstract / methodology / perChainExplainer / bestPerChain / worstPerChain from the aggregate at the page-level fetch. Per-chain compute stays on the unfiltered hub only. Net effect: page cold-start drops from 36 query sets to 9, while keeping all the chain-aware placeholders correctly resolved on every tab. * timeseries: hide hover dots and tooltip rows for excluded providers * fix(ledger): move chain coverage chips to second row so titles fit Reported visually: chips ('#1 · BASE', '#1 · BNB CHAIN', '#1 · SOLANA') were rendered inline in the same flex row as the provider logo + name + tag + type badge, eating horizontal space on the name column. Names truncated to 'Mo...', 'Cod...', 'GeckoTerm...' on the bench leaderboard. Restructure: wrap in a flex-col, keep logo/name/tag/type on top line, push the chips to a second flex-wrap row indented 28 px to align under the name. Chips still md+ only. * timeseries: per-line hover index + gap-aware dots and tooltip rows * fix(ledger): only render #1 chips on the bench leaderboard row Reported visually: on wallet-labels-coverage (11 chain dimensions), every row was showing all 11 chain chips (Ethereum, Solana, BNB Chain, Base, Arbitrum, Polygon, Optimism, TON, Stellar, XRP, Bitcoin) — most rendered as neutral 'not leading on this chain' chips. Total noise: ~44 chips across 4 rows. Default ChainCoverageChip to leaders-only mode. Non-leader chips are hard-skipped. The bench page chain tabs already provide precise per-chain rankings; this chip is a quick scan aid for 'where does this provider currently top the field' — that signal disappears when buried under 9 neutral chips per row. leadersOnly=false escape hatch kept so /products/[slug] (or any future consumer) can opt back into the full coverage view. * retire Leaderboard view from bench pages, default count/usd to Distribution * Revert "Merge pull request #193 from ChainBench/feat/retire-leaderboard-view" This reverts commit 9eeb33c601d1e73bf9582d397c82c6b6fad4602f, reversing changes made to a5e2efb4137d332a9256f1a88454da6b9ef9edb1. * distribution + ranked-bar: stop clamping sub-1 values to position 0 on log scale * distribution: position p50 dot at p50Pct% (was stuck at left:0) * distribution: redesign as percentile whisker with landmark axis * distribution: add mean marker (outlined ring) to whisker * audit: refresh MCP allowlist + fix bridge-revenue 404 source links + llms.txt count * fix(bench): wrap GitHub source link on mobile Reported visually: the 'Source code github.com/.../tree/main/harnesses/<slug>' link at the bottom of bench pages overflows the viewport on mobile (uppercase + 0.16em tracking inflates an already-80-char URL well past the screen width). Adds break-all on the <p> and max-w-full on the inline <a> so the URL wraps at any character without pushing the page horizontally. * site-header: interactive 3D logo sphere * Revert "Merge pull request #202 from ChainBench/feat/3d-site-logo" This reverts commit bbf8afa96fe9cbccf794f18a99f6dccb2e88b2d0, reversing changes made to 97754cc4836f129afb9eaeb74bf2f6d5195025f2. * chore: remove GMGN from bench-001 + assets (#200) * chore: remove GMGN from bench-001 + assets Per maintainer's request — GMGN integration is being walked back. - benchmarks/aggregator-head-lag.yml: drop the gmgn provider entry, revert methodology bullets + seo_intro mention to the pre-GMGN copy. Per-chain editorial placeholders kept (still useful for the bench). - src/data/provider-registry.ts: drop gmgn entry. - src/lib/logo-manifest.ts: drop gmgn entry. - public/logos/gmgn.svg: deleted. - harnesses/aggregator-head-lag: drop gmgn_monitor.go, GMGN_ENABLED config, GMGN startup banner + goroutine, chromium Dockerfile install, utls dep. Per-chain architecture (bestPerChain placeholders, products page chips, badge ?chain= param, sitemap per-chain entries) is left in place — it serves metadata-coverage / rpc-capabilities / bridge benches independently of GMGN. * chore: drop unrelated files accidentally included in prior commit Worktree leftovers (.claude/worktrees/), local logo-3d experiment, and dev-branch site-header edits got swept in during the GMGN removal commit. Restore them out of this branch so the PR diff is GMGN-only. * feat(bench-029): add Solana DEX quote latency spec (#203) New bench measuring wallclock latency of Solana DEX quote APIs (Jupiter, Mobula, OpenOcean, Raydium) every 60s from 3 regions. DFlow added when partnership API key is provided. Status starts as 'draft' — flip to 'live' after 24h of metric data accumulates in Prom. Pairs with mobula-monorepo#6608 (harness) + #6609 (Grafana dashboard). Metric names line up with what the harness emits (solana_quote_latency_ms histogram + solana_quote_success gauge + 3 error counters). Methodology + FAQ disclose Mobula co-location bias explicitly. * chore(harnesses): port solana-quote-latency to public repo + polish seo (#205) Source code for bench #029 (Solana DEX quote latency) was previously private in mobula-api/miniapps/. Move it here so the public source link on the bench page resolves to runnable code. Also tighten the bench seo_title/description: drop the trailing 'ms' suffix and add the 2026 year tag, matching the pattern of the more recent benches (solana-tx-landing-latency, perp-fees, etc.). * chore: update repo refs from OpenChainBench/OpenChainBench to ChainBench/OpenChainBench (#206) GitHub auto-redirects the old URL but updating in-place for cleanliness, SEO, and to keep clone instructions valid. Covers all 19 bench YAMLs, contributor docs, src/ links, and .github templates. * bench-029: flip status draft -> live (Prom has samples on all 4 providers across 3 regions) (#207) * feat(products): wire raydium + openocean logos and registry entries (#208) Logos added by user at repo root, moved to public/logos/ and wired into logo-manifest. Provider registry entries placed next to jupiter (Solana DEX cluster) for bench-029 (solana-dex-quote-latency). * bench-029: add region dimension + correct hosting claims (#209) * feat(products): wire raydium + openocean logos and registry entries Logos added by user at repo root, moved to public/logos/ and wired into logo-manifest. Provider registry entries placed next to jupiter (Solana DEX cluster) for bench-029 (solana-dex-quote-latency). * bench-029: add region dimension + correct unverified hosting claims - Add dimensions.region block so the page renders region tabs (us-east, eu-west, sgp) at the top with full-page re-fetch on click, mirroring the rpc-capabilities pattern. - Fix unverified 'Mobula on Railway us-east' claim: DNS shows all 4 providers are CDN-fronted (Cloudflare for 3, CloudFront for Jupiter), so the latency gap is edge-caching policy, not hosting region. Update the methodology disclosure, the per-provider finding, and the FAQ entry to reflect what the data actually shows. * bench-029: warm-path harness + remove false 'Mobula operates' disclosure (#210) * feat(products): wire raydium + openocean logos and registry entries Logos added by user at repo root, moved to public/logos/ and wired into logo-manifest. Provider registry entries placed next to jupiter (Solana DEX cluster) for bench-029 (solana-dex-quote-latency). * bench-029: add region dimension + correct unverified hosting claims - Add dimensions.region block so the page renders region tabs (us-east, eu-west, sgp) at the top with full-page re-fetch on click, mirroring the rpc-capabilities pattern. - Fix unverified 'Mobula on Railway us-east' claim: DNS shows all 4 providers are CDN-fronted (Cloudflare for 3, CloudFront for Jupiter), so the latency gap is edge-caching policy, not hosting region. Update the methodology disclosure, the per-provider finding, and the FAQ entry to reflect what the data actually shows. * bench-029: warm-path harness + remove false 'Mobula operates' claim Two coupled changes: 1. Harness switches from cold-path (KeepAlive: -1, DisableKeepAlives: true, new http.Client per tick) to warm-path: one persistent http.Client per provider, TCP+TLS reused across the 60s tick, HTTP/2 attempted. The measured number is now steady-state RTT — what a long-running backend integration actually sees — not the first-call cold handshake penalty. Shared helper in cmd/monitor/http_client.go. 2. Methodology + FAQ updated to reflect warm-path. Drop the inaccurate 'Mobula operates OpenChainBench' disclosure (Mobula sponsors harnesses that it hosts on its Railway infra, but does NOT operate or own the project). Keep the CDN disclosure since DNS verification confirmed all 4 providers sit behind a CDN — that fact still affects how the numbers should be read. * alternatives: pad JSON-LD description to Schema.org Dataset 50-char minimum * fix: bust bench cache keys to surface bench-029 (#212) The all-benchmarks-v6 and bench-unfiltered-v4 entries cached before bench-029 (solana-dex-quote-latency) was merged were still serving from Vercel Data Cache, so the new bench was 404 on direct URL and absent from /benchmarks search. Bumping to v5 / v7 forces a fresh build. * fix(brand): distinct provider colors + sgp region glyph (#213) - Jupiter switched from #FF7A00 (amber-orange) to #C7F284 (jupiter matrix green, their secondary brand) so it doesn't collide with mobula's vivid orange on the solana-dex-quote-latency by-region chart. - Add raydium (#00C2FF cyan) and openocean (#2563EB deep blue) so they stop falling through to the editorial palette and reading as similar orange/violet on the chart. - Add sgp brand color + isRegion entry alias to ap-southeast. The bench dimension value is the raw Railway label sgp, but the brand lookup was only registered under ap-southeast, so the SGP region tab had no globe glyph and no colored ring (rpc-capabilities had the same issue). - Rename the SGP region label from 'AP-Southeast' to 'Singapore' on both bench-029 and rpc-capabilities. AP-Southeast is AWS naming; for end users Singapore is clearer. * fix(bench): hide chart's internal region tabs when bench declares dimensions.region (#214) When a bench YAML has dimensions.region, the top-level Region row already controls region scoping. The TimeSeriesChart was still rendering its own internal region tabs because the page only forwarded the region prop when showChartRegionRow was true (the chart-only fallback path). Now we pass the top-level region selection down whenever regionOptions exist, so the chart hides its tabs and matches what the page-level filter selects. Also rename the AP-Southeast chart-tab label to 'Singapore' to match the top-level row, and add an alias for the raw 'sgp' region key. * site-header: 3D logo on desktop, SVG on touch (#215) * mobile: reliable sticky header + soften dot pattern (#216) * feat: hyperliquid frontends quality bench № 030 (draft, SQLite cohort retention) * hl-frontends: COALESCE null retention sum + METRICS_ADDR local-dev override * mobile: fixed header + dynamic spacer, dots more visible * bench-029 v2: rotate USDC → long-tail trending tokens (#220) The anchored SOL → USDC pair Jupiter was serving from CloudFront edge cache in 35 ms — that measured cache, not routing. Rotating the tokenOut every tick from Solana volume ranks 6-55 (stables excluded) defeats any per-pair edge cache and forces every provider to actually search a path. Harness changes (mirrored to OCB public repo from mobula-api PR #6613): - trending.go (new): fetches the rotation list every 10 min, picks one token at random per tick. - Provider contract adds tokenOut parameter; all 4 adapters refactored to quote USDC → tokenOut at 100 USDC notional, 1% slippage. - New counter solana_quote_no_route_total excluded from the latency histogram (liquidity gap is not a latency outlier). - isNoRouteSignal matches Jupiter NO_ROUTES_FOUND / TOKEN_NOT_TRADABLE, Mobula 'No route found' / 'Token not found', Raydium INSUFFICIENT_LIQUIDITY / ROUTE_NOT_FOUND. OpenOcean is payload-detected (outAmount==0, dexId<0, path==null, price_impact==-100%) since it has no string error. - log_buffer ring is now O(1) instead of O(n) slice-shift. YAML changes: - methodology rewritten around the rotation, no-route counter, and warm-path keep-alive measurement. - per-provider formulas tightened to the verified response field name + the no-route handling. - FAQ adds entries for the rotation rationale, the no-route counter, and Raydium's single-venue scope. * mobile: opaque bg + GPU hint on fixed header (kill notch see-through) (#221) * hyperliquid-frontends: flip status to live (page renders, awaits real builders.json addresses for data) * site-logo-switcher: layer SVG under 3D to kill the swap flash * hl-frontends: shorten disclaimer + 2 methodology bullets to satisfy Zod 500-char cap * logo-switcher: never paint SVG on desktop (CSS-gate) * bench-029 v3: switch rotation source from REST volume-ranks to Pulse V2 WS Sacha's ask was 'pulse trending tokens', not 'top by 24h volume'. v2's REST volume-ranks pick was a related-but-different signal — established mid-tier tokens with cumulative volume, not what's pumping right now. v3 actually wires Pulse V2 in. Harness changes (mirrored from mobula-api PR): - New pulse_subscriber.go opens a persistent ws://pulse-v2-api.mobula.io session, subscribes to the bonded view on Solana, dedupes by mint into a 30-min sliding pool keyed by lastSeen. Reconnect with 5s -> 60s exponential backoff. ~50-300 active mints in steady state. - trending.go now exposes Note() / Forget() (called from the WS handler) and a Pick() that prefers the pulse pool over the REST snapshot. REST refresh kept on a 10-min cron as the fallback for Pulse outages. - scheduler.go boots both goroutines, waits up to 30s for either source to fill the pool, then ticks. The 'src=PULSE|REST' log per tick makes the active source observable in real time. - gorilla/websocket added to go.mod. YAML methodology rewritten to describe the Pulse rotation. Raydium's no-route rate disclosure bumped from ~30-50% to ~80% because the bonded view is mostly Pump.fun graduates living on PumpSwap, not on Raydium-native pools. Local validation: 6 ticks observed, 5/6 sourced from PULSE, 6 distinct tokens (FARTCOIN, tolytown, cbBTC, Electricia, DUCKY, TOPLESS). Jupiter no-routes on 1, OpenOcean on 1 (Electricia), Raydium on 5 (as expected). Pool grew 50 -> 61 over the run. * hl-frontends: real builder addresses (Phantom, Axiom, pvp.trade, Insilico, Defiapp, MetaMask, Dexari, Okto) * logo-switcher: instant sphere placeholder on desktop while 3D loads * add logos + registry entries for 8 hyperliquid frontends * logo: static import 3D, drop dynamic + placeholder * hl-frontends: registry entries + logo manifest mappings (dropped from earlier squash) * fix(l1-finality): switch Litecoin from Blockchair to litecoinspace.org (#233) Blockchair's free-tier IP-blacklisted Railway's egress; the Litecoin fetcher had been returning HTTP 430 'temporary blacklisted' on every tick (359 errors/h, l1_finality_health{chain=litecoin}=0, the last_refresh_timestamp gauge was never even emitted because no fetch ever succeeded). Switched to litecoinspace.org (mempool.space-style Esplora API for Litecoin) — free, no API key required, no IP blacklist. The new fetcher is one call per tick (GET /api/blocks returns 15 recent blocks with height + unix timestamp; the 12-confirmations finalized block is index 12 of the array) vs Blockchair's two-call /stats + /dashboards flow. Fallback to a second /api/blocks/<height> call if the confirmation depth ever exceeds the default window. Verified locally: lag=1912s (= 32 min, matches 12 confirmations × 2.5 min LTC block time), fetch latency 33ms warm-path. * hl-frontends: switch headline from rolling 24h to last complete UTC day * fix: mobile overflow on benchmark detail pages - Tables (ledger, region-grid): min-w only applies from sm: so mobile shrinks the table inside its overflow-x-auto wrapper instead of forcing 480/520px and pushing the whole page past the viewport edge - count-leaderboard summary strip stacks 1-col on mobile, reduces gap-x so 'RANGE 40 → 306 7 providers' no longer cuts off - summary-stat removes the 8rem min-width on mobile flex children - breadcrumb + bench identifier lower tracking on mobile, last crumb truncates at 60vw so long titles can't push the row off-screen - Title + subtitle get break-words as safety net - html { overflow-x: clip } as a final root-level safety net * fix: site-logo mask id collides between hidden header + visible footer useId() gives each instance its own mask id. The shared 'c-mask-header' id was being defined by both the header (display:none on desktop) and the footer; Safari ignores a mask whose owning SVG is hidden, so the footer painted an unmasked filled circle instead of the C cutout. * hl-frontends: walk back 3 days to handle Hyperliquid bucket publish lag * fix(l1-finality): slow Cardano polling to 60s to stay under Koios free-tier quota Cardano was DOWN for ~96h: l1_finality_health{chain=cardano}=0, 359 errors/h via api.koios.rest. Koios responds 200 from any clean IP, but the harness on Railway hits 429/403 — Koios's free tier is 5,000 req/day/IP, and our default 10s cadence × 2 calls per tick = 17,280 req/day, which burns the quota in ~7h then blacklists the egress IP. Add a per-chain MinIntervalSeconds field to ChainConfig: when > 0, the fetchAll dispatcher skips ticks for that chain until enough time has passed. Cardano set to 60s → 2,880 req/day, safely under the quota. Other chains keep the global 10s cadence (Solana/Tron/EVM via direct RPC have no such quota). Resolution loss is minimal: Cardano blocks are ~20s and 15 confirmations takes 5 min of wall-clock to finalize, so per-minute polling is plenty. Verified locally: 2 Cardano ticks in 75s (~60s cadence), lag=478-513s (= 15 confs × ~20s + empty-slot variance), other chains still 8 ticks in 75s. * fix(l2-block-time): head-watchdog to catch ghost WS connections Scroll row went silent: samples_total stuck at 23,887 for >1h, 0 reconnects in the same period, health=1. The reconnect loop never fires because the pong handler bumps SetReadDeadline on every pong, so a provider that keeps the WS heartbeat alive while silently dropping our newHeads subscription wedges the read loop forever. Scroll already had 109 lifetime reconnects (vs 12-37 on the other 8 L2s), confirming the publicnode → Scroll WS link is the flakiest of the bunch. Add a per-connection head-watchdog goroutine: stores the timestamp of the last received newHead in an atomic int64, checks every 15s, and forces a conn.Close() if the silence exceeds 90s. Closing the conn makes ReadMessage return → runChain returns → outer reconnect loop bumps the counter and dials again. All 9 L2s have block-time under 15s, so 90s of silence is unambiguously a dead subscription. Verified locally: 6 Scroll blocks in 30s plus all 8 other L2s emitting at their natural cadences; watchdog stays quiet under normal traffic. * timeseries: interpolate hover-dot Y between sample brackets so it sits on the line The hover dot was using Math.round to snap to the nearest sample's value, which disagreed visually with the line during steep slopes. The line is drawn as straight SVG segments between samples (M…L…L…), so at hover-X between sample N and N+1 the line is at lerp(v[N], v[N+1], t) — but the dot was at either v[N] or v[N+1] depending on which way Math.round broke. Most visible on bench-029 when a provider's quote goes from 1.5s -> 200ms between two consecutive samples: dot floats above or below the descending segment. Fix: floor instead of round to get the bracketing pair, lerp the value at the fractional position. Dot now lands exactly on the rendered line. The skip-rendering rule is preserved (no dot when either bracket is a gap). * fix: stronger mobile overflow safety net for Safari + Telegram WebView - overflow-x: clip on html AND body (WKWebView ignores it on html alone) - max-width: 100vw on body to harden against any wide grandchild - break-words on SEO intro + methodology paragraphs * fix: shrink region-grid + hide extra ledger cols on mobile aggregator-head-lag (and other geo-distributed Solana benches) showed overflow on iPhone because: - region-grid cells used grid-cols-[1fr_5rem] on every viewport; with 3 regions + a 128px Product column the table intrinsic min was ~404px > 375px iPhone SE viewport. Now: grid-cols-[1fr_auto] on mobile, Product col w-20 (80px) instead of w-32, tighter gap, smaller value font. Stays at the original densities from sm: up. - ledger Slot delta and Secondary columns were visible on mobile (no hidden md:table-cell), adding 2 extra mono columns to benches with hasSlots/secondary. Now hidden on mobile like the other aggregate columns (p90/p99/Mean/Delta/Success). * fix: defensive overflow-x: clip on main + article (mobile WebView) The html/body-level clip from PR #242 is sometimes ignored by iOS WKWebView (Telegram in-app browser, certain Safari versions) when a deep descendant forces width > viewport. Adding the clip on main and article means the guard applies on a non-root box where browsers honour it consistently. Also w-full + min-w-0 + max-w-full at each level so flex/grid children can actually shrink instead of inheriting an intrinsic min-content width. LiveIndicator: drop the 'updated Ns ago' suffix on <sm screens. The inline-flex container can't wrap internally, so on a 375 px viewport the whole bench-identifier row was pushed beyond the edge. Dot + Live keeps the freshness signal without the width tax. * fix(solana-quote): align region key to sgp so SGP tab renders (#245) * chore: remove em-dashes / en-dashes from user-facing content (#246) User wants prose to read less AI-generated. Replaced all em-dashes (—) and en-dashes (–) in rendered content with commas, periods, colons or parentheses depending on context. Comment-only dashes (JSDoc, //, /* */) kept as-is since they never reach the rendered site. Files touched: 5 YAML benchmark specs, 1 markdown bench page, provider registry descriptions, OG / Twitter image alt text, alternatives SEO description, MCP API param description, donut-chart placeholder. Mobula affiliation audit: clean. The site already reads as a neutral third-party benchmark project. Mobula appears only as a benchmarked provider alongside Codex, GeckoTerminal, etc., never as parent / owner. * feat(hl-frontends): cutover to v2 (sub-minute freshness from local hl-node) (#247) Swap p50/p99/sample_size/series/mean queries to the *_v2 metrics emitted by the new hyperliquid-frontends-local harness on OVH SGP. Volume/fees/ effective_fee_bps/fills/fees_per_user now refresh every 30s instead of the 24-48h bucket lag. Keep p90 = hl_frontend_d7_retention_pct{} from v1 since v2 doesn't have SQLite retention state yet (deferred to v2.1). Replace the success metric (csv_fetch_status was specific to the bucket fetcher; v2 has no HTTP layer) with a harness-liveness heartbeat: hl_frontend_local_last_tick_unix_v2 must be < 120s old. Note: the v2 rolling 24h window started filling at 14:21 UTC today, so volumes will look low for the next ~22h until the window is fully populated. After that they'll match v1's full-day numbers. * feat(hl-frontends): refresh findings + FAQ with v2 bangers (deviation, outage, taker, per-asset) (#248) Replace the old findings block (template-based, generic) with 6 new findings that cite specific live numbers from the v2 metrics: - effective fee spread 10x across cohort (Insilico 1.00 → MetaMask 10.00) - price deviation as user-cost signal (Dexari 25.83 bps, 13x median) - volume ≠ user-friendliness (Phantom #1 volume but Insilico cheapest) - two live outage signals (Dexari 19min stale, Okto 24h inactive) - taker share separates aggressive UIs from pro terminals - asset concentration reveals routing strategy (HIP-3 stocks on Dexari/MM) Plus 2 new FAQ entries explaining what price_deviation and last_fill_age actually measure, since they are unique to this bench in the HL space. * feat(bench): switchable companion metric panels under main ledger (#249) Spec authors can now declare any number of `metric_panels` (up to 8) in a bench YAML. Each panel names one Prometheus metric; the loader queries it per provider as `<metric>{<label_key>="<slug>"}` and stores the scalar values. A new MetricPanelGrid component renders below the main table with a tab row at the top; clicking a tab re-ranks the cohort by that panel's metric, applying its higher_is_better direction. Hyperliquid frontends ships first with 5 panels: slippage proxy, time since last fill (outage), fills per minute (activity), taker share, and volume routed. All values come from the local hl-node v2 harness at sub-minute freshness. Non-breaking: benches without `metric_panels` render exactly as before. Cardinality impact on Prom is bounded (5 metrics x 8 providers = 40 new series across all panels for this bench). * feat(bench): metric view tabs swap the main chart instead of stacking tables (#250) Previous PR rendered each metric panel as a separate static table under the main chart. This commit replaces that with a tab row above the chart itself: clicking a tab swaps which metric the chart displays (per-builder multi-line, 24h trailing) so the reader stays in a single visual context and can compare trends instead of snapshots. Implementation: - spec.ts now fetches a 24h series for each panel x provider alongside the scalar values (`prom.series(query, winSec, 72)`). - MetricPanel gains an optional `seriesByProvider` field. - TimeSeriesChart accepts new optional props (seriesOverride, metricLabelOverride, unitOverride) that swap its data source + header. - New MetricViewTabs component pure presentation; parent owns active id. - benchmark-body wires the state, panel description rendered under chart. - Drops the old MetricPanelGrid table component (no longer used). Non breaking: benches without `metric_panels` render exactly as before (the tab row is conditionally rendered only when panels are declared). * fix(chart): include metric panel id in seriesKey so chart remounts on tab swap (#251) The Chart child component used `key={seriesKey}` where seriesKey only encoded range + region. When the parent swapped seriesOverride via a metric panel tab click, lines useMemo did recompute (override added to deps) but the Chart child's internal state (drawn paths, slicedLines) kept the prior render cached. Adding the active panel label to the key forces a clean remount on tab swap. * docs(hl-frontends): refresh disclaimer, seo_intro, abstract, methodology, faq for v2 pipeline (#252) Rewrites the bench documentation to describe the local hl-node fills stream rather than the deprecated daily CSV bucket. The methodology list now covers all 9 metrics actually emitted (effective fee, fees per user, price deviation, last fill age, fills per minute, taker share, per asset top three, plus the heartbeat and rolling window mechanics). FAQ entries that referenced the CSV URL or the old frontend list (OKX, GMGN, Dexterity, KGEN, ASXN) updated to match the current builders.json (Phantom, Axiom, pvp.trade, Insilico, DefiApp, MetaMask, Dexari, Okto). Source URL points at the new miniapps/hyperliquid-frontends-local harness. No em dashes anywhere in prose per content guideline. All character caps respected. * chore: safe cleanup — dead code, stale lint suppressions, unused vars (#253) Audit-driven cleanup verified to have zero visible impact on the site. Deletes (confirmed zero imports across src/): - src/components/benchmark-table.tsx (201 LOC, never imported) - src/lib/methodology-tooltip.ts (37 LOC, helper never wired up) - src/lib/utils.ts (cn() helper, never called) - CURRENT_ISSUE and ISSUE_DATE_LONG exports in site.ts Lint cleanups: - Drop three stale eslint-disable directives (apple-icon, icon, benchmarks/[slug]/page) for rules that no longer fire - Fix two unescaped apostrophes in mcp/page.tsx - Remove unused activeTemplate variable in share-section.tsx - Drop padB / padR from a useMemo deps array in time-series-chart.tsx where they were never read in the closure Kept on purpose despite audit suggestions: - .card CSS class (used 5x by card / live / benchmark-grid / body) - --color-accent-soft and --color-banner-* tokens (used by Tailwind utilities in benchmark-grid, view-switcher, home-bench-table, site-banner) - GithubIcon custom SVG (lucide-react dropped its GitHub brand mark) - Static three.js import on the 3D logo (replacing with next/dynamic re-introduces the SVG flash that PR #231 deliberately removed) - Hook-rule and ref-during-render lints in benchmark-body / time-series-chart: real refactors, out of scope for a zero-impact cleanup, deferred to a follow-up Verified: pnpm typecheck clean, pnpm build green. * hl-frontends: expand providers 8 → 60 (DefiLlama registry sync) (#254) * chore: remove SiteBanner (#256) User reports the banner leaves a phantom gap in Telegram WebView even once dismissed (localStorage doesn't always persist in in-app browsers, and the wrapper's padding still measures even when the banner returns null). Drop the component, drop the import, drop the spacer's banner- related comment + the now-irrelevant initial height estimate. * chart: add Top 5/10/20/All selector when cohort > 10 providers (#257) * chart: size top-N selector off registered cohort, not emitted series (#258) * chore: delete verified dead code (real pass, not agent guesses) (#255) This time I greped every file myself instead of trusting agents. Deletes (zero usages confirmed across src/): - src/components/live-dashboard.tsx — 6-line re-export shim; inlined the import in app/page.tsx so the shim has nothing left to redirect. - src/lib/brand.ts: brandIsDark() — never called anywhere. - src/lib/live/format.ts: fmtAge() — never called. live-indicator.tsx has its own formatAge() with day handling that fmtAge lacks, so they weren't an actual duplicate either. - src/components/chain-coverage-chip.tsx: leaderChains(), perChainRankSegments(), chainLabelFor(), chainLeader() — exported but never imported AND never called from within the same file. TypeScript noUnusedLocals confirms they were stranded. Visibility tightened: - src/lib/brand.ts: isLight() now non-exported (only used by chipTextColor in the same file). Drive-by cleanup that fell out: - Dropped ProviderResult import in chain-coverage-chip.tsx (only the deleted helpers used it). Verified clean: pnpm typecheck + pnpm tsc --noEmit --noUnusedLocals --noUnusedParameters + pnpm build all pass. Net: -79 lines. * fix: drop overflow-x clip from body so position: fixed sticks on WKWebView (#259) User reports a 1-2 px gap at the top of the header in Telegram in-app Safari that lets page content peek through during scroll. Root cause: body had overflow-x: clip (PR #242 safety net) and WebKit treats clip on body as creating a containing block for position: fixed descendants. The fixed header was then anchored to body's top edge, not the viewport, and momentum scroll desynced it from the URL-bar animation, producing the visible slit. Move the overflow-x clip safety net to <html> only. <main> and <article> already carry overflow-x-clip (PR #244) so page-level horizontal overflow remains contained without needing it on body. Drop the translate3d hint on the header — it was paired with the clip workaround and is not needed once fixed positions cleanly against the viewport. * hl-frontends: 60-builder registry, real logos, methodology refresh, availability backstop (#260) * hotfix(hl-frontends): trim methodology line 8 to <=500 chars (Zod limit) (#261) * fix: switch header to position: sticky (iOS 26 WebKit fixed jitter bug) (#262) Web search confirmed user is not alone. WebKit bug 297779 reports that on iOS 26 Safari + WKWebView (Telegram in-app browser, Chrome iOS), any position: fixed element drifts 10-24 px when scroll direction reverses, exposing the page under the header through a thin gap. Mastodon and LinkedIn ship the same symptom. Apple acknowledged the bug, partially fixed it in iOS 26.1 but residual reports persist into Dec 2025. position: sticky uses a different render path in WebKit that does not trip the jitter heuristic. It is also simpler: - no spacer div needed (the element stays in normal flow) - no useRef + ResizeObserver to keep the spacer in sync - no headerH state Net: -25 lines on top of fixing the cosmetic gap. Desktop and standalone Safari behavior are identical, sticky just renders more reliably in WebViews. Refs: - https://bugs.webkit.org/show_bug.cgi?id=297779 - https://github.com/mastodon/mastodon/issues/36144 - https://github.com/TelegramMessenger/Telegram-iOS/issues/1748 * fix: drop rows with zero headline value from ledger and ranked-bar (#264) * fix: switch header to position: sticky (iOS 26 WebKit fixed jitter bug) Web search confirmed user is not alone. WebKit bug 297779 reports that on iOS 26 Safari + WKWebView (Telegram in-app browser, Chrome iOS), any position: fixed element drifts 10-24 px when scroll direction reverses, exposing the page under the header through a thin gap. Mastodon and LinkedIn ship the same symptom. Apple acknowledged the bug, partially fixed it in iOS 26.1 but residual reports persist into Dec 2025. position: sticky uses a different render path in WebKit that does not trip the jitter heuristic. It is also simpler: - no spacer div needed (the element stays in normal flow) - no useRef + ResizeObserver to keep the spacer in sync - no headerH state Net: -25 lines on top of fixing the cosmetic gap. Desktop and standalone Safari behavior are identical, sticky just renders more reliably in WebViews. Refs: - https://bugs.webkit.org/show_bug.cgi?id=297779 - https://github.com/mastodon/mastodon/issues/36144 - https://github.com/TelegramMessenger/Telegram-iOS/issues/1748 * ranked-bar+ledger: add Top-N selector and drop unscored rows * ledger+ranked-bar: drop rows with zero headline value across p50/p90/p99 * chart: slice seriesOverride by range so panel tabs respect 1h/6h (#265) * chart: Top-N buttons reflect available data (hide useless options) (#266) * fix: drop sticky on mobile (iOS 26 WebKit jitter workaround) (#267) position: sticky and position: fixed both jitter 10-24 px on iOS 26 when scroll direction changes (bugs.webkit.org/297779). Apple acknowledged the bug, partial fix in 26.1, residual reports into late 2025. Mastodon and LinkedIn ship the same symptom and concluded it is unfixable from the site side. Mitigation: keep sticky only on md+ where the desktop render path is clean. On mobile the header stays in normal flow, scrolls away with the page, and the jitter cannot appear because nothing is positioned. Same pattern as most mobile sites — pull to top to see the nav. Single class change: `sticky top-0` -> `relative md:sticky md:top-0`. * fix: keep sticky header on regular mobile, only drop it in iOS WebViews (#268) Previous PR (#267) dropped sticky on all mobile to dodge the iOS 26 WebKit jitter bug. Overkill — the symptom only manifests visibly inside in-app WebViews (Telegram, Instagram, Facebook, Twitter, TikTok) where the host's own URL-bar overlay animates on top of WebKit's misposition. Regular mobile Safari and Chrome iOS render sticky cleanly enough for end users. Mechanism: - Inline script in layout.tsx head detects iOS WebView via UA (no `Safari/` token on an iOS device = WKWebView in-app host) and adds the `ios-webview` class on <html> before React hydrates. - globals.css scopes a non-sticky override to that class on the site header at < md viewports. - site-header.tsx exposes a stable identifier class (`site-header-root`) and stays sticky-top-0 by default everywhere else. No flash on first paint because the class lands during the same render tick as the dark-mode detection script next to it. Build + typecheck clean. * fix(chart): Top-N selector on all 4 chart views (ranked, time, distribution, donut) (#269) * feat(bench): network fees — current native transfer cost USD across 11 L1 chains * chart: Top-N selector on all 4 chart views via shared hook+component * ledger: follow active panel — sort/filter/value per panel metric (#271) * wallet-labels-coverage: drop All chains aggregate tab (#272) * feat(bench): network fees — current native transfer cost USD across 11 L1 chains * chart: Top-N selector on all 4 chart views via shared hook+component * wallet-labels-coverage: drop All chains aggregate tab * feat(bench): network fees — current native transfer cost USD across 11 L1 chains (#270) * bench-body: pass activePanel to LedgerTable + dynamic ledger title (#273) * hotfix(bench-body): restore ledger title template literal + closing brace (#276) * feat(network-fees): L1/L2 toggle + 9 L2 providers (#278) * fix(network-fees): granular USD format + split L1/L2 ledgers (#279) * fix(network-fees): granular USD format + split L1/L2 ledgers * chore: gitignore agent worktrees * sync(topN): lift Top-N to parent so chart and ledger share one value (#280) * chore: delete verified dead exports (audit rerun) (#274) Re-greped each export in src/lib against the rest of src/. Deletes (0 references confirmed): - leaderForChain() in citation.ts — defined when per-chain headline rewrites were planned, never wired up. - fmtCount() in live/format.ts — fmtCountFull is what ticker.tsx uses; the short variant has no callers. Tightened visibility (only used inside the defining file): - AlternativeWithBench (alternatives.ts) export -> internal type - stripFaqMarkdown (jsonld.ts) export -> internal helper - chainIdFragment (jsonld.ts) export -> internal helper pnpm exec tsc --noEmit --noUnusedLocals --noUnusedParameters: EXIT=0 pnpm build: green. * fix(network-fees): top layer toggle + drop top-N selector (#281) * sync(topN): lift Top-N to parent so chart and ledger share one value * fix(network-fees): top-of-page layer toggle, drop top-N selector * fix(bench-body): restore topNControl + ledger topN wiring (dropped in #281) (#282) * feat(network-fees): SEO rewrite, target user questions, no hyphens (#283) * sync(topN): lift Top-N to parent so chart and ledger share one value * feat(network-fees): SEO rewrite + scope clarification, drop hyphens * fix(ledger): rescale sparkline min/max off active panel series (#284) * sync(topN): lift Top-N to parent so chart and ledger share one value * feat(network-fees): SEO rewrite + scope clarification, drop hyphens * ledger: rescale sparkline min/max off active panel series * hl-frontends(seo): pro tone, brand names in description, drop stale OKX/GMGN mentions (#285) * hl-frontends(seo): feature FOMO in description + 2 new FAQ entries (#287) * fix(bench): bump maxDuration to 60s for 20+ provider benches (#286) * sync(topN): lift Top-N to parent so chart and ledger share one value * fix(bench): maxDuration 60s for cold start with 20+ providers * feat(hl-frontends): pivot headline to USD revenue + 7d/30d ranges (#289) * sync(topN): lift Top-N to parent so chart and ledger share one value * docs(network-fees): drop the chain count from subtitle + seo description User reads cleaner without the number; the list of chains right after it already says which 20 are tracked. * feat(hl-frontends): pivot headline to USD revenue, add 7d/30d ranges * fix(hl-frontends): collapse redundant fee/volume panels, use chart range tabs (#290) * sync(topN): lift Top-N to parent so chart and ledger share one value * fix(hl-frontends): collapse fees/volume time variants — use chart range tabs instead * fix(hl-frontends): mark volume and users panels as higher_is_better so ledger sorts descending (#291) * fix(build): remove leftover merge conflict markers (#292) * sync(topN): lift Top-N to parent so chart and ledger share one value * fix(build): remove leftover merge conflict markers in chart components * fix(build): really remove leftover merge conflict markers (#293) * fix(chart): anchor partial series on right edge, stop stretching across full range (#294) * fix(build): really remove leftover merge conflict markers * fix(chart): anchor partial series on right edge instead of stretching across full range * fix(hl-frontends): swap p90 metric to unlock 60-builder cohort (#295) p90 was hl_frontend_d7_retention_pct which only the legacy 8-builder Railway harness emits. spec.ts skips any provider with a null p90 or p99, so 52 of 60 builders (including the top earners Phantom Perps $83K, Rabby $15K, OneKey $12K, based.app $12K) were silently dropped from the leaderboard. Switching p90 to hl_frontend_volume_usd_24h_v2 (60 series) restores the full cohort. * fix(chart): y-axis tick formatter handles usd + count (#296) * fix(hl-frontends): swap p90 metric to unlock 60-builder cohort p90 was hl_frontend_d7_retention_pct which only the legacy 8-builder Railway harness emits. spec.ts skips any provider with a null p90 or p99, so 52 of 60 builders (including the top earners Phantom Perps $83K, Rabby $15K, OneKey $12K, based.app $12K) were silently dropped from the leaderboard. Switching p90 to hl_frontend_volume_usd_24h_v2 (60 series) restores the full cohort. * fix(chart): y-axis tick formatter now handles usd + count units * fix(chart): panels fetch 24h+7d+30d series so range tabs show real data span (#297) * fix(hl-frontends): swap p90 metric to unlock 60-builder cohort p90 was hl_frontend_d7_retention_pct which only the legacy 8-builder Railway harness emits. spec.ts skips any provider with a null p90 or p99, so 52 of 60 builders (including the top earners Phantom Perps $83K, Rabby $15K, OneKey $12K, based.app $12K) were silently dropped from the leaderboard. Switching p90 to hl_frontend_volume_usd_24h_v2 (60 series) restores the full cohort. * fix(chart): y-axis tick formatter now handles usd + count units * fix(chart): panels fetch 24h+7d+30d series so range tabs show right data span * fix(chart): Top N ranking respects higher_is_better, matching ledger (#298) * fix(hl-frontends): swap p90 metric to unlock 60-builder cohort p90 was hl_frontend_d7_retention_pct which only the legacy 8-builder Railway harness emits. spec.ts skips any provider with a null p90 or p99, so 52 of 60 builders (including the top earners Phantom Perps $83K, Rabby $15K, OneKey $12K, based.app $12K) were silently dropped from the leaderboard. Switching p90 to hl_frontend_volume_usd_24h_v2 (60 series) restores the full cohort. * fix(chart): y-axis tick formatter now handles usd + count units * fix(chart): panels fetch 24h+7d+30d series so range tabs show right data span * fix(chart): Top N ranking respects higher_is_better, matching ledger sort * feat(wallet-labels): kind dimension toggle (contract vs eoa) (#304) * docs(wallet-labels): explain EOA vs contract kind split in methodology + FAQ (#305) * feat(bench-032): add pm-data-freshness bench (Polymarket T0 vs Codex vs Mobula) (#308) * feat(logo): add Polymarket provider logo (#309) * chore(mcp): allowlist pm_ prefix for the pm-freshness bench (#310) * fix(pm-freshness): scrape via Railway internal DNS not public URL (#311) * fix(pm-freshness): sub-ms floor for Polymarket T0 row so it shows on the chart (#312) * chore(pm-freshness): short provider names + drop em dashes in editorial copy (#313) * fix(bench): Best/Worst headline respects higher_is_better (#314) For benches like HL frontends (higher_is_better=true, fees in USD), Best is the max value not the min, and Worst is the min not the max. Latency benches keep the original min=Best mapping. Fixes BEST=$0 / WORST=$24K on staging HL frontends page where the leaders (phantom-perps $119K, metamask $66K) were being mapped to Worst. * release: exclude held-back benches (hyperliquid-frontends, solana-dex-quote-latency, solana-tx-landing*, wallet-labels-coverage)
16 tasks
Flotapponnier
added a commit
that referenced
this pull request
Jun 24, 2026
- Linear-inspired chrome: hairline border, single soft shadow, no
accent ring, snappy 150ms open animation (fade + slide-down 4px).
- New useRecentSearches hook (localStorage, ocb:search:recent:v1
namespace, cap 7, custom-event same-tab sync, useSyncExternalStore
for hydration safety).
- Idle state surfaces 3 sections:
- Recent chips (horizontal scroll with edge fade, X to remove)
- Live leaders cards (horizontal snap, fetched from /api/citable,
shows current #1 provider + p50 value with logo)
- Trending list (vertical, same data shape as featured)
- Search state: results grouped by kind, each row gets the leader's
logo (benches) or entity logo (products/chains), or a kind-icon
fallback for compares/answers/alternatives/pages.
- Mobile: full-bleed sheet, kbd footer hidden, X close button.
- /api/citable fetched once on mount; 300s edge cache means usually
zero network hit. Renders gracefully without it (logo + title only,
no p50 / leader).
This was referenced Jun 24, 2026
This was referenced Jul 3, 2026
Flotapponnier
pushed a commit
that referenced
this pull request
Jul 5, 2026
Replaces the identical templated intro paragraph on both pages with
a data-driven prose summary derived from live measurements. Kills the
last piece of duplicate above-the-fold text so Bing sees genuinely
unique substantive content per URL.
/compare/[slug]:
Before: 'Side by side OpenChainBench measurements. Identical layout,
no editorial verdict...' (same on 247 pages)
After: 'Codex leads on 2 of 4 shared benchmarks, Mobula on 2. Codex
wins on aggregator-head-lag (128ms vs 195ms), wallet-labels-
coverage (42.8% vs 38.1%). Mobula wins on metadata-coverage
(96.9% vs 89.1%), network-coverage (80 vs 42 chains).'
/products/[slug]:
Before: 'Moralis performance benchmarks, live across 2 categories.
Reproducible measurements, open methodology.' (same on 104 pages)
After: 'Moralis is measured across 2 live OpenChainBench benchmarks,
with 1 #1 finish: NFT collection metadata (ranks #1, 96.9% p50),
Wallet labels coverage (ranks #2 of 5, 42.4% p50).'
Falls back to a neutral sentence when the provider's p50 data is
missing (cold ISR, harness restart) so cold pages don't render a lie.
7 tasks
Flotapponnier
added a commit
that referenced
this pull request
Jul 5, 2026
…ompare (#908) * seo: purge thin compare pages + benchmark-first title on products & compare Bing WMT audit 2026-07-05: only 2 of 5093 sitemap URLs indexed. Root cause: 4938 ad-hoc /compare/ pages emitted at ≥1 shared bench threshold, producing near-duplicate templates that starved crawl budget and hurt domain trust. Changes: - sitemap: hybrid threshold. Brand-whitelist pairs emit at ≥1 shared, others at ≥3. Drops from 4938 → ~226 ad-hoc + 21 curated = ~247 URLs. Preserves every commercial X vs Y pair users actually search for (helius-vs-mobula, alchemy-vs-moralis, chain-vs-chain, perp-vs-perp). - compare-pairs: remove jupiter-vs-raydium (raydium has 0 bench appearances so /compare/jupiter-vs-raydium 404s at render). - products/[slug]: title now leads with 'Benchmark {year}' + head-term match on '<provider> benchmark' queries. Meta description ends with 'As of YYYY-MM-DD' for LLM citation extractability (LLM-mediated discovery drives ~80% of Bing query traffic). - compare/[slug]: title now leads with '{a} vs {b} Benchmark {year}'. Meta description is unique per pair (shared bench count + date) killing the identical duplicate signal that had Bing skipping pages. * seo: prose summary above the fold on /compare + /products Replaces the identical templated intro paragraph on both pages with a data-driven prose summary derived from live measurements. Kills the last piece of duplicate above-the-fold text so Bing sees genuinely unique substantive content per URL. /compare/[slug]: Before: 'Side by side OpenChainBench measurements. Identical layout, no editorial verdict...' (same on 247 pages) After: 'Codex leads on 2 of 4 shared benchmarks, Mobula on 2. Codex wins on aggregator-head-lag (128ms vs 195ms), wallet-labels- coverage (42.8% vs 38.1%). Mobula wins on metadata-coverage (96.9% vs 89.1%), network-coverage (80 vs 42 chains).' /products/[slug]: Before: 'Moralis performance benchmarks, live across 2 categories. Reproducible measurements, open methodology.' (same on 104 pages) After: 'Moralis is measured across 2 live OpenChainBench benchmarks, with 1 #1 finish: NFT collection metadata (ranks #1, 96.9% p50), Wallet labels coverage (ranks #2 of 5, 42.4% p50).' Falls back to a neutral sentence when the provider's p50 data is missing (cold ISR, harness restart) so cold pages don't render a lie. * chore(lint): exclude standalone sub-apps from main frontend lint infrastructure/monitoring-ui and infrastructure/prom-admin are independently deployed Next.js apps (own package.json + Railway config, own build/lint/typecheck pipeline). Scanning them from the main frontend's lint pass caused every unrelated PR to fail on their pre-existing warnings that the sub-app maintainers can fix in their own dedicated PRs. worker/ is the materialize worker (own package.json + Railway config), same reasoning. Unblocks PR #908 (SEO purge) and any subsequent PR touching only src/*. --------- Co-authored-by: Florent Tapponnier <contact@mobula.io>
Flotapponnier
added a commit
that referenced
this pull request
Jul 5, 2026
…ompare (#908) (#909) * seo: purge thin compare pages + benchmark-first title on products & compare Bing WMT audit 2026-07-05: only 2 of 5093 sitemap URLs indexed. Root cause: 4938 ad-hoc /compare/ pages emitted at ≥1 shared bench threshold, producing near-duplicate templates that starved crawl budget and hurt domain trust. Changes: - sitemap: hybrid threshold. Brand-whitelist pairs emit at ≥1 shared, others at ≥3. Drops from 4938 → ~226 ad-hoc + 21 curated = ~247 URLs. Preserves every commercial X vs Y pair users actually search for (helius-vs-mobula, alchemy-vs-moralis, chain-vs-chain, perp-vs-perp). - compare-pairs: remove jupiter-vs-raydium (raydium has 0 bench appearances so /compare/jupiter-vs-raydium 404s at render). - products/[slug]: title now leads with 'Benchmark {year}' + head-term match on '<provider> benchmark' queries. Meta description ends with 'As of YYYY-MM-DD' for LLM citation extractability (LLM-mediated discovery drives ~80% of Bing query traffic). - compare/[slug]: title now leads with '{a} vs {b} Benchmark {year}'. Meta description is unique per pair (shared bench count + date) killing the identical duplicate signal that had Bing skipping pages. * seo: prose summary above the fold on /compare + /products Replaces the identical templated intro paragraph on both pages with a data-driven prose summary derived from live measurements. Kills the last piece of duplicate above-the-fold text so Bing sees genuinely unique substantive content per URL. /compare/[slug]: Before: 'Side by side OpenChainBench measurements. Identical layout, no editorial verdict...' (same on 247 pages) After: 'Codex leads on 2 of 4 shared benchmarks, Mobula on 2. Codex wins on aggregator-head-lag (128ms vs 195ms), wallet-labels- coverage (42.8% vs 38.1%). Mobula wins on metadata-coverage (96.9% vs 89.1%), network-coverage (80 vs 42 chains).' /products/[slug]: Before: 'Moralis performance benchmarks, live across 2 categories. Reproducible measurements, open methodology.' (same on 104 pages) After: 'Moralis is measured across 2 live OpenChainBench benchmarks, with 1 #1 finish: NFT collection metadata (ranks #1, 96.9% p50), Wallet labels coverage (ranks #2 of 5, 42.4% p50).' Falls back to a neutral sentence when the provider's p50 data is missing (cold ISR, harness restart) so cold pages don't render a lie. * chore(lint): exclude standalone sub-apps from main frontend lint infrastructure/monitoring-ui and infrastructure/prom-admin are independently deployed Next.js apps (own package.json + Railway config, own build/lint/typecheck pipeline). Scanning them from the main frontend's lint pass caused every unrelated PR to fail on their pre-existing warnings that the sub-app maintainers can fix in their own dedicated PRs. worker/ is the materialize worker (own package.json + Railway config), same reasoning. Unblocks PR #908 (SEO purge) and any subsequent PR touching only src/*. --------- Co-authored-by: Florent Tapponnier <contact@mobula.io>
3 tasks
Flotapponnier
added a commit
that referenced
this pull request
Jul 6, 2026
Symptom: alertmanager fires 'geckoterminal head_lag stale on bnb' every few hours across all 3 regions in both staging and prod (observed 2026-07-06). Root cause: the BNB pool configured across 3 harnesses uses PoolID '24' which points to WBNB/BUSD PancakeSwap V2 — near-idle since Binance stopped issuing BUSD in 2024. Staging-sgp logs show 2 gecko/bnb samples per 5000 log lines vs 7 for base and 1 for solana; the sub-5min sample cadence the head_lag rule expects is impossible on a dead pool. Fix: replace with PancakeSwap V3 USDT/WBNB 0.01% fee tier, currently the #1 BSC pool by volume ($130M+ 24h). Internal GT pool_id = 160787671 (derived from app.geckoterminal.com/api/p1/bsc/pools/0x172fcd41e0913e95784454622d1c3724f546f849). Applied to all 3 harnesses that hardcode the same list: - aggregator-head-lag (fires the alert) - aggregator-latency-benchmark - metadata-coverage Co-authored-by: Florent Tapponnier <contact@mobula.io>
Flotapponnier
added a commit
that referenced
this pull request
Jul 6, 2026
… (#921) Symptom: alertmanager fires 'geckoterminal head_lag stale on bnb' every few hours across all 3 regions in both staging and prod (observed 2026-07-06). Root cause: the BNB pool configured across 3 harnesses uses PoolID '24' which points to WBNB/BUSD PancakeSwap V2 — near-idle since Binance stopped issuing BUSD in 2024. Staging-sgp logs show 2 gecko/bnb samples per 5000 log lines vs 7 for base and 1 for solana; the sub-5min sample cadence the head_lag rule expects is impossible on a dead pool. Fix: replace with PancakeSwap V3 USDT/WBNB 0.01% fee tier, currently the #1 BSC pool by volume ($130M+ 24h). Internal GT pool_id = 160787671 (derived from app.geckoterminal.com/api/p1/bsc/pools/0x172fcd41e0913e95784454622d1c3724f546f849). Applied to all 3 harnesses that hardcode the same list: - aggregator-head-lag (fires the alert) - aggregator-latency-benchmark - metadata-coverage Co-authored-by: Florent Tapponnier <contact@mobula.io>
5 tasks
Flotapponnier
pushed a commit
that referenced
this pull request
Jul 15, 2026
Two bench-correctness fixes surfaced by an audit of the oracle and gas surfaces we plan to cite in outreach. Bench 025 (oracle-deviation): - sample_size was 'count(count by (source) (ocb_oracle_price))' which returns the count of distinct sources (3 or 4), not the count of samples over the window. The citable API therefore reported sampleSize=3-4 on every pair, at odds with the spec's 'polled every 30s' claim. Rewrites the query to 'sum(count_over_time(ocb_oracle_price[24h]))' across all 10 pair entries so the number reflects actual poll volume. Leader ranking: - citationCandidates() now excludes providers with a measured success rate below 50 percent from the leader pool. Before this fix, gas-estimation showed Owlracle as #1 with successRate=6.48 and a p50 gap of 0.001 gwei: technically accurate on the rare successful poll but useless as a leader claim. Falls back to the full live pool when every provider is under the floor so a totally degraded bench still surfaces a best-of-bad-options leader rather than vanishing. Freshness benches without a success query default to 100 percent in the loader so the guard is inert there. Adds src/lib/citation.test.ts with 5 tests covering the Owlracle scenario, higher-is-better ranking, and the degraded-pool fallback.
Flotapponnier
added a commit
that referenced
this pull request
Jul 15, 2026
* fix(bench): sample-size PromQL + leader reliability floor Two bench-correctness fixes surfaced by an audit of the oracle and gas surfaces we plan to cite in outreach. Bench 025 (oracle-deviation): - sample_size was 'count(count by (source) (ocb_oracle_price))' which returns the count of distinct sources (3 or 4), not the count of samples over the window. The citable API therefore reported sampleSize=3-4 on every pair, at odds with the spec's 'polled every 30s' claim. Rewrites the query to 'sum(count_over_time(ocb_oracle_price[24h]))' across all 10 pair entries so the number reflects actual poll volume. Leader ranking: - citationCandidates() now excludes providers with a measured success rate below 50 percent from the leader pool. Before this fix, gas-estimation showed Owlracle as #1 with successRate=6.48 and a p50 gap of 0.001 gwei: technically accurate on the rare successful poll but useless as a leader claim. Falls back to the full live pool when every provider is under the floor so a totally degraded bench still surfaces a best-of-bad-options leader rather than vanishing. Freshness benches without a success query default to 100 percent in the loader so the guard is inert there. Adds src/lib/citation.test.ts with 5 tests covering the Owlracle scenario, higher-is-better ranking, and the degraded-pool fallback. * fix(bench): propagate reliability floor to rankings + template + hub Follow-up on the previous commit. Review flagged that filtering the leader without filtering the ranking arrays leaves self-contradictory JSON: /api/stat can report leader=Etherscan while rankings[0]=Owlracle on the same document. Exports citationCandidates and adds rankedCandidates helper that returns the same eligible pool sorted by the bench's higher_is_better direction. leader() and fieldValue() now share it. Wires the shared helper into every surface that emits a rankings-like list or a best-of claim: - src/app/api/stat/[slug]: rankings array now sorted from rankedCandidates so rankings[0].slug always equals leader.slug on the same JSON. - src/app/api/llm-context: the Rankings numbered list under each bench matches the Headline sentence above it. - src/app/api/mcp/[transport]: get_benchmark tool response and the openchainbench://benchmark/{slug} resource both share the pool. - src/lib/bench-template: {{best_name}} / {{best_p50}} / {{worst_name}} tokens in bench copy resolve against the same pool so a bench body never crowns a provider its own headline excludes. Per-slug tokens {{p50:some-slug}} still resolve against the raw live pool so unknown tokens still fall through. - src/lib/providers: rankProviders (drives /products hub wins count and per-chain leadership chips) now runs on the reliable pool. Adds a consistency test locking that rankedCandidates[0].slug always matches leader().slug on the same bench. Total: 6 tests in citation.test.ts, 95/95 pass across src/. Uses (r.successRate ?? 100) for the numeric guard to future-proof against ProviderResult shapes that skip the field. Load path still defaults to 100 (materialize/load.ts:748) so this is inert today, but the extra ?? removes the fragility. /api/compare/[a]/[b] is intentionally left unfiltered: the user picks the two providers explicitly, so the head-to-head is a requested comparison rather than a leader claim. Applying the floor there would drop rows the user explicitly asked to see. * fix(hub-card): honor citation leader in the hub grid headline Follow-up review flagged that benchmark-card.tsx re-sorted b.results locally and picked results[0], so a bench where citation demotes the raw-p50 winner (e.g. Owlracle on gas-estimation) would show the demoted provider on the hub grid while the bench page names the correct leader. Two documents contradicting each other in one click. Adds leaderSlug: string | null to BenchmarkCardData and populates it at the projection boundary via leader(b), which already applies the reliability + insufficient-sample filter. The card picks the leader by matching that slug in the projected results, and falls back to the raw best when the projection returned no leader (draft, insufficient, or all providers filtered out) so a card still renders a headline value in every state. Kept the local sort so the fallback path and any downstream chip using sorted[0].name keeps working. Test suite unchanged: 95/95 pass across src/. No test needed since this is a projection-boundary wire-up. --------- Co-authored-by: Florent Tapponnier <contact@mobula.io>
Flotapponnier
added a commit
that referenced
this pull request
Jul 16, 2026
) * fix(bench): sample-size PromQL + leader reliability floor Two bench-correctness fixes surfaced by an audit of the oracle and gas surfaces we plan to cite in outreach. Bench 025 (oracle-deviation): - sample_size was 'count(count by (source) (ocb_oracle_price))' which returns the count of distinct sources (3 or 4), not the count of samples over the window. The citable API therefore reported sampleSize=3-4 on every pair, at odds with the spec's 'polled every 30s' claim. Rewrites the query to 'sum(count_over_time(ocb_oracle_price[24h]))' across all 10 pair entries so the number reflects actual poll volume. Leader ranking: - citationCandidates() now excludes providers with a measured success rate below 50 percent from the leader pool. Before this fix, gas-estimation showed Owlracle as #1 with successRate=6.48 and a p50 gap of 0.001 gwei: technically accurate on the rare successful poll but useless as a leader claim. Falls back to the full live pool when every provider is under the floor so a totally degraded bench still surfaces a best-of-bad-options leader rather than vanishing. Freshness benches without a success query default to 100 percent in the loader so the guard is inert there. Adds src/lib/citation.test.ts with 5 tests covering the Owlracle scenario, higher-is-better ranking, and the degraded-pool fallback. * fix(bench): propagate reliability floor to rankings + template + hub Follow-up on the previous commit. Review flagged that filtering the leader without filtering the ranking arrays leaves self-contradictory JSON: /api/stat can report leader=Etherscan while rankings[0]=Owlracle on the same document. Exports citationCandidates and adds rankedCandidates helper that returns the same eligible pool sorted by the bench's higher_is_better direction. leader() and fieldValue() now share it. Wires the shared helper into every surface that emits a rankings-like list or a best-of claim: - src/app/api/stat/[slug]: rankings array now sorted from rankedCandidates so rankings[0].slug always equals leader.slug on the same JSON. - src/app/api/llm-context: the Rankings numbered list under each bench matches the Headline sentence above it. - src/app/api/mcp/[transport]: get_benchmark tool response and the openchainbench://benchmark/{slug} resource both share the pool. - src/lib/bench-template: {{best_name}} / {{best_p50}} / {{worst_name}} tokens in bench copy resolve against the same pool so a bench body never crowns a provider its own headline excludes. Per-slug tokens {{p50:some-slug}} still resolve against the raw live pool so unknown tokens still fall through. - src/lib/providers: rankProviders (drives /products hub wins count and per-chain leadership chips) now runs on the reliable pool. Adds a consistency test locking that rankedCandidates[0].slug always matches leader().slug on the same bench. Total: 6 tests in citation.test.ts, 95/95 pass across src/. Uses (r.successRate ?? 100) for the numeric guard to future-proof against ProviderResult shapes that skip the field. Load path still defaults to 100 (materialize/load.ts:748) so this is inert today, but the extra ?? removes the fragility. /api/compare/[a]/[b] is intentionally left unfiltered: the user picks the two providers explicitly, so the head-to-head is a requested comparison rather than a leader claim. Applying the floor there would drop rows the user explicitly asked to see. * fix(hub-card): honor citation leader in the hub grid headline Follow-up review flagged that benchmark-card.tsx re-sorted b.results locally and picked results[0], so a bench where citation demotes the raw-p50 winner (e.g. Owlracle on gas-estimation) would show the demoted provider on the hub grid while the bench page names the correct leader. Two documents contradicting each other in one click. Adds leaderSlug: string | null to BenchmarkCardData and populates it at the projection boundary via leader(b), which already applies the reliability + insufficient-sample filter. The card picks the leader by matching that slug in the projected results, and falls back to the raw best when the projection returned no leader (draft, insufficient, or all providers filtered out) so a card still renders a headline value in every state. Kept the local sort so the fallback path and any downstream chip using sorted[0].name keeps working. Test suite unchanged: 95/95 pass across src/. No test needed since this is a projection-boundary wire-up. --------- Co-authored-by: Florent Tapponnier <contact@mobula.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Header tagline removed, hero copy more product-y, section labels updated.