From cd36bd42069e616ba7ba023a1de5de0adf0a85f8 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:45:38 +0200 Subject: [PATCH 1/3] fix(l1-finality): exclude stale-gauge rows from ranking + dynamic seo meta - Add MIN_DISPLAY_SUCCESS_PCT=5 in provider-filters; Prom gauges retain last value on harness failure so a chain at 0% success but non-zero stale p50 ranked #1 on lower-is-better benches - Apply filter in ledger-table sortedAll, ranked-bar-chart scored, and [chain]/page sortLive so all display surfaces agree - Guard buildKeyFacts against stale-gauge edge case (p50>0, bad rate) - Add citation_* meta + robots/twitter to per-chain generateMetadata (parity with parent bench page for academic/LLM citation) - seo_description uses {{best_name}}/{{best_p50}} so meta reflects live data instead of a hardcoded chain list --- benchmarks/l1-finality.yml | 2 +- src/app/benchmarks/[slug]/[chain]/page.tsx | 34 ++++++++++++++++++---- src/components/ledger-table.tsx | 7 +++++ src/components/ranked-bar-chart.tsx | 4 ++- src/lib/provider-filters.ts | 24 +++++++++++++++ 5 files changed, 64 insertions(+), 7 deletions(-) diff --git a/benchmarks/l1-finality.yml b/benchmarks/l1-finality.yml index 73ba0d3c..d8536e2f 100644 --- a/benchmarks/l1-finality.yml +++ b/benchmarks/l1-finality.yml @@ -4,7 +4,7 @@ slug: l1-finality number: "006" title: Fastest L1 blockchain finality, live across 11 chains seo_title: "Fastest L1 finality 2026" -seo_description: "Fastest L1 blockchain finality measured live across 11 chains: Gram, SUI, Hedera, Stellar, Solana, Ethereum, Bitcoin and more. Real times, not whitepaper claims." +seo_description: "{{best_name}} leads L1 finality at {{best_p50}} (p50, 24h) across 11 chains. Ethereum, Solana, Bitcoin, SUI, Gram — live wall-clock measurements, not whitepaper claims." subtitle: Wall-clock seconds from latest block to the finalized block on Ethereum, Solana, Bitcoin, Gram, SUI, Stellar and 5 more chains, refreshed every 10 seconds. seo_intro: | This page measures L1 finality time live for every major Layer-1 blockchain, with p50 / p90 / p99 refreshed every 10 seconds. Stellar finality time is ~5 seconds, the close interval the Stellar Consensus Protocol locks in via federated Byzantine agreement. Solana finality time goes from sub-second on the processed commitment to ~12.8 s on finalized after 32 confirmed slots. Ethereum finality time is about 12.8 minutes in the ideal case, the 2-epoch Casper FFG window; observed wall-clock finality runs longer, and the table below shows the live measurement. Hedera finality time clears in 3-5 seconds via Hashgraph aBFT. SUI finality time and Gram finality time (formerly TON) both sit under one second via Mysticeti DAG-BFT and BAG consensus. BNB and Avalanche finality time land near two seconds through fast-finality forks. Probabilistic chains (Bitcoin, Litecoin, Monero) settle on a confirmation-depth convention measured here in minutes. Bitcoin finality time is roughly one hour at the industry-standard 6 confirmations; the protocol itself never reaches absolute finality. diff --git a/src/app/benchmarks/[slug]/[chain]/page.tsx b/src/app/benchmarks/[slug]/[chain]/page.tsx index 7564acef..cd9d9a35 100644 --- a/src/app/benchmarks/[slug]/[chain]/page.tsx +++ b/src/app/benchmarks/[slug]/[chain]/page.tsx @@ -5,7 +5,7 @@ import { ArrowLeft, ArrowUpRight } from "lucide-react"; import { getBenchmark } from "@/data/benchmarks"; import { Breadcrumb } from "@/components/breadcrumb"; import { citableAsOf } from "@/lib/citation"; -import { liveResults } from "@/lib/provider-filters"; +import { liveResults, MIN_DISPLAY_SUCCESS_PCT } from "@/lib/provider-filters"; import { fmtUnit } from "@/lib/format"; import { capDescription } from "@/lib/seo-text"; import { getBenchCreatedAt } from "@/lib/seo/bench-dates"; @@ -115,9 +115,11 @@ function resolveLeftoverPlaceholders(text: string, b: Benchmark): string { } function sortLive(results: ProviderResult[], higherIsBetter: boolean) { - return [...liveResults(results)].sort((a, b) => - higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50, - ); + return [...liveResults(results)] + .filter((r) => (r.successRate ?? 100) >= MIN_DISPLAY_SUCCESS_PCT) + .sort((a, b) => + higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50, + ); } async function loadChainPage( @@ -254,7 +256,9 @@ function buildKeyFacts(data: ChainPageData): string { if (data.shape === "row") { const { result, sorted, rank } = data; const subject = `${result.name} ${metricPhrase(b.metric)}`; - if (result.ms.p50 <= 0) { + const hasGoodData = result.ms.p50 > 0 && + (result.successRate == null || result.successRate >= MIN_DISPLAY_SUCCESS_PCT); + if (!hasGoodData) { return `${subject} is measured continuously on this benchmark. Live numbers will appear here as soon as the harness reports fresh samples.`; } const p50 = fmtUnit(result.ms.p50, b.unit); @@ -303,23 +307,43 @@ export async function generateMetadata({ ); const canonical = `${SITE.url}/benchmarks/${data.benchmark.slug}/${chain}`; const ogImage = `${SITE.url}/api/og/${data.benchmark.slug}`; + const b = data.benchmark; + const isDraft = b.status === "draft"; + const isAwaiting = isDraft && b.editorialStatus === "live"; + const isoPubDate = b.lastRunAt + ? new Date(b.lastRunAt).toISOString().slice(0, 10) + : undefined; return { title, description, alternates: { canonical }, + ...(isAwaiting ? { robots: { index: false, follow: true } } : {}), openGraph: { title, description, type: "article", url: canonical, + siteName: SITE.name, images: [ogImage], }, twitter: { card: "summary_large_image", + site: SITE.twitter, title, description, images: [ogImage], }, + other: { + citation_title: title, + citation_author: "OpenChainBench", + citation_publisher: "OpenChainBench", + ...(isoPubDate ? { citation_publication_date: isoPubDate } : {}), + citation_doi: "10.5281/zenodo.20800312", + citation_pdf_url: `${SITE.url}/api/stat/${b.slug}`, + citation_public_url: canonical, + citation_language: "en", + citation_journal_title: "OpenChainBench", + }, }; } diff --git a/src/components/ledger-table.tsx b/src/components/ledger-table.tsx index e46bfcb3..8f1b2896 100644 --- a/src/components/ledger-table.tsx +++ b/src/components/ledger-table.tsx @@ -18,6 +18,7 @@ import { ProviderLogo } from "@/components/provider-logo"; import { ProviderTypeBadge } from "@/components/provider-type-badge"; import { fmtUnit } from "@/lib/format"; import { buildProviderColors } from "@/lib/series-colors"; +import { MIN_DISPLAY_SUCCESS_PCT } from "@/lib/provider-filters"; import { isRegion } from "@/lib/brand"; import { isAll } from "@/lib/dimensions"; @@ -270,6 +271,12 @@ export function LedgerTable({ // undersized field. "low" rows stay; the row renderer shows them // with a soft pill instead. if (r.dataConfidence === "insufficient") return false; + // Success-rate gate. Prometheus gauges retain the last measured + // value when the harness fails, so a row with a stale non-zero p50 + // but 0 % real success rate would otherwise rank as #1 on a + // lower-is-better bench. Excluded rows join unresponsiveRows for + // display; they are not silently hidden. + if ((r.successRate ?? 100) < MIN_DISPLAY_SUCCESS_PCT) return false; if (activePanel) { const v = activePanel.values[r.slug]; return v != null && Number.isFinite(v) && v !== 0; diff --git a/src/components/ranked-bar-chart.tsx b/src/components/ranked-bar-chart.tsx index 3eeb5a53..51e55500 100644 --- a/src/components/ranked-bar-chart.tsx +++ b/src/components/ranked-bar-chart.tsx @@ -4,6 +4,7 @@ import { useMemo, useRef, useState } from "react"; import type { Benchmark } from "@/types/benchmark"; import { fmtUnit } from "@/lib/format"; import { buildProviderColors } from "@/lib/series-colors"; +import { MIN_DISPLAY_SUCCESS_PCT } from "@/lib/provider-filters"; import { useChartExclusion } from "@/hooks/use-chart-exclusion"; import { useTopN } from "@/hooks/use-top-n"; import { LiveDot } from "@/components/live-dot"; @@ -64,7 +65,8 @@ export function RankedBarChart({ // surface doesn't open with a long stack of empty bars that tie at // the bottom (or, when lower-is-better, falsely lead the ranking). const scored = benchmark.results.filter( - (r) => r.ms.p50 > 0 || r.ms.p90 > 0 || r.ms.p99 > 0 + (r) => (r.ms.p50 > 0 || r.ms.p90 > 0 || r.ms.p99 > 0) && + (r.successRate ?? 100) >= MIN_DISPLAY_SUCCESS_PCT ); const sorted = scored.sort((a, b) => benchmark.higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50 diff --git a/src/lib/provider-filters.ts b/src/lib/provider-filters.ts index bb078b35..62c17b72 100644 --- a/src/lib/provider-filters.ts +++ b/src/lib/provider-filters.ts @@ -21,3 +21,27 @@ export function liveResults(results: ProviderResult[]): ProviderResult[] { (r) => r.availability !== "unavailable" && r.ms.p50 > 0, ); } + +/** + * Minimum success rate (0-100) for a row to appear in the ranked + * display surfaces (ledger table, bar chart, per-chain comparison). + * Prometheus gauges retain their last value when the harness fails, + * so a chain that once measured <1 s but has been dead for 24 h + * still holds a non-zero p50 while its success rate collapses to 0 %. + * The all-zero filter in liveResults does not catch this case; this + * floor does. Kept deliberately low (5 %) so occasionally-flaky rows + * are still shown, while completely dead ones (0 %, 0.28 %) are + * excluded from the ranking. citation.ts uses a separate 50 % floor + * for the citable leader claim; display and citation thresholds are + * intentionally decoupled. + */ +export const MIN_DISPLAY_SUCCESS_PCT = 5; + +/** liveResults filtered by the display success floor. Use for ranked + * surfaces (ledger, bar chart, per-chain pages). Do NOT use in the + * citation path — citation.ts applies its own 50 % floor separately. */ +export function displayResults(results: ProviderResult[]): ProviderResult[] { + return liveResults(results).filter( + (r) => (r.successRate ?? 100) >= MIN_DISPLAY_SUCCESS_PCT, + ); +} From 777b5a5be49a23a51dbe422b8ef810adee4bc499 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:46:54 +0200 Subject: [PATCH 2/3] fix: replace em dash in seo_description (validator) --- benchmarks/l1-finality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/l1-finality.yml b/benchmarks/l1-finality.yml index d8536e2f..b8889763 100644 --- a/benchmarks/l1-finality.yml +++ b/benchmarks/l1-finality.yml @@ -4,7 +4,7 @@ slug: l1-finality number: "006" title: Fastest L1 blockchain finality, live across 11 chains seo_title: "Fastest L1 finality 2026" -seo_description: "{{best_name}} leads L1 finality at {{best_p50}} (p50, 24h) across 11 chains. Ethereum, Solana, Bitcoin, SUI, Gram — live wall-clock measurements, not whitepaper claims." +seo_description: "{{best_name}} leads L1 finality at {{best_p50}} (p50, 24h) across 11 chains. Ethereum, Solana, Bitcoin, SUI, Gram: live wall-clock measurements, not whitepaper claims." subtitle: Wall-clock seconds from latest block to the finalized block on Ethereum, Solana, Bitcoin, Gram, SUI, Stellar and 5 more chains, refreshed every 10 seconds. seo_intro: | This page measures L1 finality time live for every major Layer-1 blockchain, with p50 / p90 / p99 refreshed every 10 seconds. Stellar finality time is ~5 seconds, the close interval the Stellar Consensus Protocol locks in via federated Byzantine agreement. Solana finality time goes from sub-second on the processed commitment to ~12.8 s on finalized after 32 confirmed slots. Ethereum finality time is about 12.8 minutes in the ideal case, the 2-epoch Casper FFG window; observed wall-clock finality runs longer, and the table below shows the live measurement. Hedera finality time clears in 3-5 seconds via Hashgraph aBFT. SUI finality time and Gram finality time (formerly TON) both sit under one second via Mysticeti DAG-BFT and BAG consensus. BNB and Avalanche finality time land near two seconds through fast-finality forks. Probabilistic chains (Bitcoin, Litecoin, Monero) settle on a confirmation-depth convention measured here in minutes. Bitcoin finality time is roughly one hour at the industry-standard 6 confirmations; the protocol itself never reaches absolute finality. From 7ee31bbfc13662226c29d33e45560f43f45bc08a Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:54:06 +0200 Subject: [PATCH 3/3] fix(test): update hl-history test slugs to canonical form --- src/app/api/bench/hyperliquid-frontends/history/route.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/bench/hyperliquid-frontends/history/route.test.ts b/src/app/api/bench/hyperliquid-frontends/history/route.test.ts index f17eb47b..fc95439f 100644 --- a/src/app/api/bench/hyperliquid-frontends/history/route.test.ts +++ b/src/app/api/bench/hyperliquid-frontends/history/route.test.ts @@ -211,7 +211,7 @@ describe("GET /api/bench/hyperliquid-frontends/history", () => { source: "archive", rows: [ { - slug: "0xphantom", + slug: "phantom", name: "Phantom", volume_usd: 9_000_000, fees_usd: 90_000, @@ -219,7 +219,7 @@ describe("GET /api/bench/hyperliquid-frontends/history", () => { rank: 1, }, { - slug: "0xmetamask", + slug: "metamask", volume_usd: 4_500_000, rank: 2, },