diff --git a/benchmarks/evm-quote-latency.yml b/benchmarks/evm-quote-latency.yml
index ddafbf19..0e0b39e2 100644
--- a/benchmarks/evm-quote-latency.yml
+++ b/benchmarks/evm-quote-latency.yml
@@ -78,6 +78,10 @@ dimensions:
- { value: eu-west, label: EU West }
- { value: sgp, label: Singapore }
+# Exact per-cell (chain x region) rankings for scoped badges. One query,
+# one sample per (provider, chain, region) cell.
+rank_matrix_query: histogram_quantile(0.50, sum by (provider, chain, region, le) (rate(evm_swap_quote_latency_ms_bucket[24h])))
+
providers:
- slug: mobula
name: Mobula
diff --git a/benchmarks/oracle-deviation.yml b/benchmarks/oracle-deviation.yml
index 2fce93e0..8c9391f7 100644
--- a/benchmarks/oracle-deviation.yml
+++ b/benchmarks/oracle-deviation.yml
@@ -69,7 +69,7 @@ methodology:
- "Chainlink round age. The on-chain `updatedAt` timestamp from `latestRoundData()` is published separately as `ocb_oracle_last_round_age_seconds{source=\"chainlink\", pair}`. Chainlink updates only on deviation (typically 0.25-0.5% for blue chips) or heartbeat (~1 h), so a 30-minute age on a quiet ETH minute is normal; a 2-hour age on SOL during a volatile minute is the actual signal."
- "USDT ≈ USD assumption. Binance only quotes USDT pairs (BTCUSDT, etc.). We treat USDT as ≈ USD ± 10 bps drift, which is acceptable for a bench whose alert floor is ≥ 10 bps. A real USDT depeg would surface as Binance drifting from the other three sources for *every* pair simultaneously, exactly what we want this bench to flag, not hide."
- "MATIC → POL migration. Polygon migrated MATIC → POL 1:1 on Sep 4 2024. The Chainlink mainnet feed contract is still named MATIC/USD but on-chain `description()` confirms it tracks the POL token; Pyth renamed the feed to POL/USD; Coinbase delisted MATIC-USD and only lists POL-USD; Binance kept MATICUSDT as a frozen historical pair AND lists POLUSDT. We point Binance at POLUSDT so all four sources track the same underlying asset. Bench label is kept as `pair=\"MATIC/USD\"` for query continuity."
- - "Time-aligned deviation (canonical headline). For every pair of sources (a, b) we anchor on the more recent of their two SourceTSs (Chainlink's on-chain `updatedAt` for Chainlink, fetch time for the continuously-updating sources) and look up the older source's price in a 30-minute per-source rolling history at the anchor moment. The result is published as `ocb_oracle_deviation_at_oracle_ts_pct` and feeds the leaderboard headline `ocb_oracle_max_deviation_pct`. This eliminates the artifact where Chainlink's heartbeat lag inflates 'deviation' by the market's drift between its updates — a researcher grading oracle quality wants to compare Chainlink's price against the market at Chainlink's own updatedAt, not at the harness's fetch instant. The legacy fetch-time gauge (`ocb_oracle_deviation_pct`, aliased as `ocb_oracle_deviation_at_fetch_ts_pct`) is preserved unchanged for backward compatibility. Alignment misses (no history sample within ±10s of the anchor) are counted as `ocb_oracle_alignment_miss_total`. Methodology recommendation from Coinpaprika data team review; matches the convention published by Chaos Labs and Risk DAO oracle risk reports."
+ - "Time-aligned deviation (canonical headline). For every source pair we anchor on the more recent SourceTS (Chainlink's on-chain `updatedAt`, fetch time for continuous sources) and look up the older source's price in a 30-minute rolling history at that moment. Published as `ocb_oracle_deviation_at_oracle_ts_pct`, feeding `ocb_oracle_max_deviation_pct`. Removes Chainlink heartbeat-lag inflating deviation; alignment misses count as `ocb_oracle_alignment_miss_total`."
- "Excluded by design. Redstone (push-pull, no continuous gauge to scrape without integration contract), Uniswap V3 TWAP (per-pool integration + derivation of same CEX prints), DIA (smaller footprint than the four kept), and aggregator-of-aggregators (CoinGecko, CMC, DefiLlama), re-aggregating already-aggregated data adds latency and hides per-source disagreement."
findings:
diff --git a/benchmarks/rpc-capabilities.yml b/benchmarks/rpc-capabilities.yml
index d791c114..ea579bac 100644
--- a/benchmarks/rpc-capabilities.yml
+++ b/benchmarks/rpc-capabilities.yml
@@ -124,6 +124,12 @@ dimensions:
- { value: eu-west, label: EU-West }
- { value: sgp, label: Singapore }
+# One instant query feeding the exact per-cell (chain x region) rankings
+# behind scoped badges and leadership claims. Cross-region averages hide
+# region-restricted leaders (dRPC wins every chain from sgp only), so
+# badge scoping reads these cells instead of the aggregate ranks.
+rank_matrix_query: avg by (provider, chain, region) (quantile_over_time(0.50, rpc_latency_milliseconds[24h]))
+
# Real metrics emitted by the rpc-capabilities harness:
# rpc_latency_milliseconds{provider, chain} gauge
# rpc_latency_milliseconds_histogram{provider, chain} histogram
diff --git a/benchmarks/solana-dex-quote-latency.yml b/benchmarks/solana-dex-quote-latency.yml
index 248612ec..5a1dfb26 100644
--- a/benchmarks/solana-dex-quote-latency.yml
+++ b/benchmarks/solana-dex-quote-latency.yml
@@ -76,6 +76,10 @@ dimensions:
- { value: eu-west, label: EU-West }
- { value: sgp, label: Singapore }
+# Exact per-region rankings for scoped badges. One query, one sample per
+# (provider, region) cell.
+rank_matrix_query: histogram_quantile(0.50, sum by (provider, region, le) (rate(solana_quote_latency_ms_bucket[24h])))
+
faq:
- q: "Which Solana DEX has the fastest quote API right now?"
a: "{{best_name}} currently returns quotes the fastest at {{best_p50}} (p50, 24 h) across {{count}} measured providers. The leaderboard re-sorts every 60 seconds against fresh samples and rotates the target token each tick so no provider can serve from edge cache. The answer reflects 24 hours of measured latency across us-east, eu-west and sgp, not a marketing-page claim."
diff --git a/benchmarks/stablecoin-peg.yml b/benchmarks/stablecoin-peg.yml
index 89dd6e18..58a074ac 100644
--- a/benchmarks/stablecoin-peg.yml
+++ b/benchmarks/stablecoin-peg.yml
@@ -58,12 +58,12 @@ methodology:
- "Sources per stable, USDC: Binance USDC/USDT (USDT-anchored, secondary), Kraken USDCUSD, Bitstamp usdcusd. USDT: Kraken USDTUSD, Bitstamp usdtusd. FDUSD: Binance FDUSDUSDT (USDT-anchored, secondary). USDe: Binance USDEUSDT (USDT-anchored, secondary). DAI: Curve 3pool `get_dy` forward (USDC → DAI) and reverse (DAI → USDC)."
- "Cadence: 5 seconds for CEX REST tickers, 12 seconds for Curve `get_dy` on-chain (matches Ethereum block time). Per-venue samples are grouped into 60-second buckets for the liquidity-weighted median aggregation."
- "Aggregation: per-minute liquidity-weighted median across USD-quoted venues (`Quote = QuoteUSD`). Weight is 24 h USD volume estimate. The aggregated price drives `peg_deviation_bps` (per-minute median, retained for backward compatibility) and `peg_deviation_bps_histogram`."
- - "Primary headline: `peg_deviation_worst_bps` — the per-minute MAX |price − $1.00| across every venue sample in the 60-second bucket, in basis points. The leaderboard ranks stables on `quantile_over_time(0.99, peg_deviation_worst_bps[24h])`. Switching from per-minute median to per-minute max surfaces the sub-minute depeg wicks (e.g. a 5-second print to $0.92 on one venue) that the median would smooth into invisibility — matching the OHLC convention used by every TradFi reference rate. The companion gauges `peg_minute_min_bps`, `peg_minute_open_bps` and `peg_minute_close_bps` expose the full bar shape so consumers can reconstruct the within-minute distribution."
+ - "Primary headline: `peg_deviation_worst_bps`, the per-minute MAX |price - $1.00| across all venue samples in the 60-second bucket, in basis points. The leaderboard ranks stables on `quantile_over_time(0.99, peg_deviation_worst_bps[24h])`. Per-minute max surfaces sub-minute depeg wicks that a median would smooth away, matching the OHLC convention of TradFi reference rates. Gauges `peg_minute_min_bps`, `peg_minute_open_bps`, `peg_minute_close_bps` expose the bar shape."
- "Cross-venue gap (OCB-only number): `max(price across USD-quoted venues), min(price)`, per minute, in basis points. Surfaces the windows where Coinbase quotes $1.0003 while Kraken quotes $0.9978. Stored as `peg_cross_venue_gap_bps`."
- "USDT-anchored secondary metric: Binance USDC/USDT, FDUSD/USDT and USDE/USDT are exposed on `peg_deviation_usdt_anchored_bps{venue}` separately so the USD-anchored primary leaderboard is not contaminated by USDT's own peg deviation."
- "Time outside band: total seconds in the trailing 24 h during which the per-minute aggregated price fell outside [0.995, 1.005] (±50 bps). Split into `peg_time_below_peg_24h_seconds` (< 0.995) and `peg_time_above_peg_24h_seconds` (> 1.005) because Circle redemption only clears above-peg, so the direction tells you which failure mode is active."
- "Depeg event flag: binary `peg_depeg_event_flag` set to 1 when the per-minute aggregated price has been outside [0.97, 1.03] for ≥5 consecutive minutes; cleared after 30 minutes back inside. Conservative so it does not flap during normal stress events."
- - "Outlier rule (multi-venue consensus): a single sample more than 2% off peg is kept only when at least one OTHER venue has also been outside the same band in the same direction within the last 30 seconds. Single-venue glitches (one CEX returns a stale or fat-finger print while every other venue is at $1.00) are dropped as `dropped_isolated`. Multi-venue confirmation (Kraken AND Bitstamp both at $0.87 = real depeg) is kept as `kept_corroborated` so the percentile metric surfaces the event. Replaces the previous flat 20% drop / 10% cap which would have clipped USDC at $0.87 during the March 2023 SVB depeg to $0.90, erasing the event in the percentile metric. Sanity floor: anything more than 50% off peg is treated as a parser bug and dropped regardless. Methodology recommendation from Coinpaprika data team review."
+ - "Outlier rule (multi-venue consensus): a sample more than 2% off peg is kept only when at least one other venue was outside the same band in the same direction within the last 30 seconds. Isolated single-venue glitches are dropped as `dropped_isolated`; corroborated moves (Kraken AND Bitstamp both at $0.87) are kept as `kept_corroborated`, so a real depeg like USDC during SVB in March 2023 survives into the percentile metric. Anything more than 50% off peg is treated as a parser bug and dropped."
- "Excluded by design: aggregator-only prices (CoinGecko, Coinmarketcap, DefiLlama) because they are themselves liquidity-weighted medians of the venues we already poll directly. Algo-stables that have already failed (UST, USDR) are out of scope; the bench tracks live, currently-redeemable stables."
findings:
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
index 07bda322..202c7bd5 100644
--- a/src/app/about/page.tsx
+++ b/src/app/about/page.tsx
@@ -52,7 +52,29 @@ export default function AboutPage() {
13 live benchmarks. ~150 (provider × chain) probe pairs. Every metric is queryable on the public Prometheus and reproducible from the harness source.
-
+
+
+ OpenChainBench is funded by Mobula because we needed honest infrastructure benchmark data internally and saw no neutral source existed. The site is open-sourced because the industry needs it.
+
+
+ Mobula competes in five live benchmarks: aggregator head lag, bridge fee, bridge quote latency, network coverage and metadata coverage. Mobula currently leads aggregator head lag and bridge quote latency. This is a real conflict of interest and we want it visible above the fold, not buried.
+
+
+ Three safeguards stand against the conflict:
+
+
+
·Open Prometheus data. Every number on the site is a literal quantile_over_time query. Anyone can hit our /api/citable or /api/stat/<slug> endpoint and re-derive the leaderboard with their own aggregation. If Mobula were inflating its rank, the raw data would show it.
+
·Open harness source. Every harness is on GitHub under harnesses/. Clone, run docker compose up, your /metrics endpoint emits the same numbers ours does within 30 seconds.
+
·Public methodology review. We invite external review and ship the fixes publicly. In June 2026 the Coinpaprika data team flagged four issues on the stablecoin peg and oracle deviation benches. We shipped three of them in pull requests #349, #352 and #353 within twenty-four hours and pushed back on the fourth with citations to CME, Chainlink and CoinGecko convention.
+
+
+ We do not run a paid tier. We do not sell ranking slots. We do not take provider sponsorship in exchange for inclusion. There is no token. If you spot any deviation from this policy, file a private security advisory and we will treat it as the integrity incident it would be.
+
+
+ Hosting and infrastructure costs are paid by Mobula. The site runs on Vercel; harnesses run on Railway. We are open to grant funding that preserves editorial independence, but we will not accept funding from any party we benchmark.
+
+
+
Every benchmark is a YAML spec plus a harness. The spec describes what to measure, which providers, which Prometheus queries hold the numbers; the harness runs continuously on Railway and exposes those metrics. A single shared Prometheus scrapes every harness; the site queries Prometheus directly and re-renders every minute. Every provider is rendered with equal visual weight. readers do their own ranking.
@@ -62,7 +84,7 @@ export default function AboutPage() {
walks through the steps. New providers, new metrics, new chains. all welcome via pull request.
-
+
·Read the{" "}live benchmarks{" "}.
·Reproduce any number. the{" "}methodology{" "}page tells you how.
@@ -70,7 +92,7 @@ export default function AboutPage() {
- {sorted.filter((a) => a.rank === 1).map((a) => {
+ {badgeCards.map((card) => {
// Absolute URL is the one shipped to embedders (it has to
// work from any third-party origin), but the in-page preview
// uses a relative path so it loads under the current
@@ -411,25 +535,56 @@ export default async function ProviderPage({
// shows the browser's broken-image glyph on every non-prod
// origin (staging Preview URLs, Vercel branch previews, etc.)
// because the CSP refuses the cross-origin fetch.
- const badgePath = `/api/badge/${a.benchmark.slug}/${p.slug}`;
+ const scopeParams = new URLSearchParams();
+ if (card.chain) scopeParams.set("chain", card.chain.value);
+ if (card.region) scopeParams.set("region", card.region.value);
+ const qs = scopeParams.size > 0 ? `?${scopeParams.toString()}` : "";
+ const badgePath = `/api/badge/${card.benchSlug}/${p.slug}${qs}`;
const badgeUrl = `${SITE.url}${badgePath}`;
- const targetUrl = `${SITE.url}/benchmarks/${a.benchmark.slug}`;
- const html = ``;
+ const targetUrl = `${SITE.url}/benchmarks/${card.benchSlug}${qs}`;
+ const scopeSuffix = `${card.chain ? ` on ${card.chain.label}` : ""}${card.region ? ` from ${card.region.label}` : ""}`;
+ const scopeLabels = [card.chain?.label, card.region?.label]
+ .filter(Boolean)
+ .join(" · ");
+ const cardTitle = scopeLabels
+ ? `${card.title} — ${scopeLabels}`
+ : card.title;
+ const altText = `Ranked #1 on OpenChainBench: ${card.title}${scopeSuffix}`;
+ const html = ``;
+ const markdown = `[](${targetUrl})`;
+ // Pre-baked X intent. Providers click → tweet draft opens
+ // with the ranking claim, the bench URL and the OCB handle
+ // already filled in. Removes the friction of writing the
+ // post themselves and gives us the canonical anchor text
+ // back as a tagged tweet on every share.
+ const tweetText = `Independently benchmarked #1 on ${card.title}${scopeSuffix} by @OpenChainBench.\n\nReproducible methodology, live data:`;
+ const tweetIntent = `https://x.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(targetUrl)}`;
return (
-
Copy HTML
@@ -438,6 +593,14 @@ export default async function ProviderPage({
{html}
+
+
+ Copy Markdown
+
+
+{markdown}
+
+
);
})}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
index a3c155ee..fa188316 100644
--- a/src/app/sitemap.ts
+++ b/src/app/sitemap.ts
@@ -94,16 +94,15 @@ export default async function sitemap(): Promise {
{ url: `${SITE.url}/press`, lastModified: pageMtime("press/page.tsx"), changeFrequency: "monthly", priority: 0.4 },
];
- // Bench routes. The hub URL (no query string) is the canonical entry
- // and ranks highest. Per-chain variants (`?chain=X`) are emitted as
- // secondary URLs so Google's crawler discovers the chain-honest
- // metadata / OG card pairs for each filter. We skip the "all" sentinel
- // (which maps to the canonical hub) and any chain dimension whose
- // value would collide with the hub after URL-encoding. Each variant
- // shares the parent bench's `lastModified` because the chain filter
- // doesn't change the underlying scrape cadence — they all refresh as
- // a single Prom poll. Priority is dropped one tier on variants so
- // Search Console reads the hub as the head of the cluster.
+ // Bench routes. The hub URL is the canonical entry and ranks highest.
+ // `?chain=X` query variants are deliberately NOT emitted: those URLs
+ // declare a canonical pointing at the unfiltered hub, so listing them
+ // told Google to index pages that self-identify as duplicates (GSC
+ // filed them under "Duplicate, Google chose different canonical").
+ // The indexable per-chain surface is the dedicated route
+ // `/benchmarks//`, generated only for chains that carry a
+ // hand-written `per_chain_explainer` entry (unique editorial content,
+ // self-canonical). See src/app/benchmarks/[slug]/[chain]/page.tsx.
const benchmarkRoutes: MetadataRoute.Sitemap = benchmarks.flatMap((b) => {
const last = b.lastRunAt ? new Date(b.lastRunAt) : BUILD_TIME;
const entries: MetadataRoute.Sitemap = [
@@ -114,15 +113,14 @@ export default async function sitemap(): Promise {
priority: 0.95,
},
];
- const chains = (b.dimensions?.chain ?? []).filter(
- (c) => c.value && c.value.toLowerCase() !== "all",
- );
- for (const c of chains) {
+ const resultSlugs = new Set(b.results.map((r) => r.slug));
+ for (const e of b.perChainExplainer ?? []) {
+ if (!resultSlugs.has(e.slug)) continue;
entries.push({
- url: `${SITE.url}/benchmarks/${b.slug}?chain=${encodeURIComponent(c.value)}`,
+ url: `${SITE.url}/benchmarks/${b.slug}/${e.slug}`,
lastModified: last,
changeFrequency: "hourly",
- priority: 0.8,
+ priority: 0.85,
});
}
return entries;
diff --git a/src/components/chain-headings-summary.tsx b/src/components/chain-headings-summary.tsx
index 9c348ca0..75f7c6c2 100644
--- a/src/components/chain-headings-summary.tsx
+++ b/src/components/chain-headings-summary.tsx
@@ -1,3 +1,4 @@
+import Link from "next/link";
import type { Benchmark } from "@/types/benchmark";
import { liveResults } from "@/lib/provider-filters";
import { fmtUnit } from "@/lib/format";
@@ -69,7 +70,19 @@ export function ChainHeadingsSummary({ benchmark }: { benchmark: Benchmark }) {
return (
- {heading}
+ {/* Chains with an explainer have a dedicated landing page
+ (/benchmarks//); the heading links there so
+ crawlers discover the per-chain documents from the hub. */}
+ {explainer ? (
+
+ {heading}
+
+ ) : (
+ heading
+ )}
diff --git a/src/lib/providers.ts b/src/lib/providers.ts
index 461a27b3..e528c2c2 100644
--- a/src/lib/providers.ts
+++ b/src/lib/providers.ts
@@ -106,6 +106,15 @@ export type ProviderAppearance = {
* can compute per-chain rank for this provider without a full bench
* re-fetch. */
bestPerChain?: Record;
+ /** Region dimension values from the spec, when present. Mirrors
+ * `benchmark.dimensions.region`. */
+ regionDimensions?: { value: string; label: string }[];
+ /** Exact per-cell rankings from the bench's `rank_matrix_query`
+ * (key = `|`, "all" for an undeclared dimension or a
+ * derived marginal). When present this is the authoritative source
+ * for scoped leadership claims — per-chain ranks built from
+ * cross-region averages hide region-restricted leaders. */
+ cellRanks?: Benchmark["cellRanks"];
};
result: ProviderResult;
rank: number;
@@ -279,6 +288,8 @@ export const getProviders = cache(async (): Promise => {
lastRunAt: b.lastRunAt,
chainDimensions: b.dimensions?.chain,
bestPerChain: benchBestPerChain,
+ regionDimensions: b.dimensions?.region,
+ cellRanks: b.cellRanks,
},
result: r,
rank: isRanked ? (idx as number) + 1 : 0,
diff --git a/src/lib/snapshot.ts b/src/lib/snapshot.ts
index 5f5b4746..b30ef125 100644
--- a/src/lib/snapshot.ts
+++ b/src/lib/snapshot.ts
@@ -33,7 +33,12 @@
*/
import { z } from "zod";
-import type { Benchmark, ProviderResult, ResultExtras } from "@/types/benchmark";
+import type {
+ Benchmark,
+ CellRankEntry,
+ ProviderResult,
+ ResultExtras,
+} from "@/types/benchmark";
/** Refuse snapshots older than this on read. 24 h matches the bench
* query window — a value older than that isn't meaningful as the
@@ -60,6 +65,7 @@ const SnapshotSchema = z.object({
bestPerChain: z.record(z.string(), z.any()).optional(),
worstPerChain: z.record(z.string(), z.any()).optional(),
providersPerChain: z.record(z.string(), z.array(z.string())).optional(),
+ cellRanks: z.record(z.string(), z.any()).optional(),
});
export type SnapshotPayload = {
@@ -70,6 +76,7 @@ export type SnapshotPayload = {
bestPerChain?: Record;
worstPerChain?: Record;
providersPerChain?: Record;
+ cellRanks?: Record;
};
function isConfigured(): boolean {
@@ -183,6 +190,9 @@ export async function readSnapshot(
providersPerChain: parsed.data.providersPerChain as
| Record
| undefined,
+ cellRanks: parsed.data.cellRanks as
+ | Record
+ | undefined,
};
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
@@ -215,5 +225,6 @@ export function snapshotFromBenchmark(b: Benchmark): SnapshotPayload {
worstPerChain: b.worstPerChain,
providersPerChain: (b as { providersPerChain?: Record })
.providersPerChain,
+ cellRanks: b.cellRanks,
};
}
diff --git a/src/lib/spec-schema.ts b/src/lib/spec-schema.ts
index 611247f5..20aa3a11 100644
--- a/src/lib/spec-schema.ts
+++ b/src/lib/spec-schema.ts
@@ -318,6 +318,22 @@ export const SpecSchema = z
})
.optional(),
+ /* Optional single PromQL returning one instant sample per
+ * (provider[, chain][, region]) — e.g.
+ * avg by (provider, chain, region) (quantile_over_time(0.50, m[24h]))
+ * Powers exact per-cell rankings (badge scoping, leadership claims)
+ * in ONE Prom roundtrip instead of a chains × regions query fan-out.
+ * Label values must match provider slugs / dimension values. */
+ rank_matrix_query: z
+ .string()
+ .min(1)
+ .max(2000)
+ .refine(
+ (q) => q.includes("provider"),
+ "rank_matrix_query must group by the provider label"
+ )
+ .optional(),
+
providers: z.array(provider).min(1),
/**
@@ -349,7 +365,24 @@ export const SpecSchema = z
.max(8)
.optional(),
})
- .strict();
+ .strict()
+ .superRefine((spec, ctx) => {
+ // A region-dimensioned bench without exact cell rankings would fall
+ // back to cross-region-average badge logic, which is precisely the
+ // bias the matrix exists to fix (a provider winning from one region
+ // reads as the global leader). Refuse the spec instead.
+ const realRegions = (spec.dimensions?.region ?? []).filter(
+ (r) => r.value !== "all",
+ );
+ if (realRegions.length > 0 && !spec.rank_matrix_query) {
+ ctx.addIssue({
+ code: "custom",
+ path: ["rank_matrix_query"],
+ message:
+ "Benches declaring dimensions.region must provide rank_matrix_query so badge claims are scoped per region",
+ });
+ }
+ });
export type Spec = z.infer;
export type SpecProvider = z.infer;
diff --git a/src/lib/spec.ts b/src/lib/spec.ts
index b6c6160c..535c21a5 100644
--- a/src/lib/spec.ts
+++ b/src/lib/spec.ts
@@ -14,7 +14,12 @@ import path from "node:path";
import { cache } from "react";
import { unstable_cache } from "next/cache";
import yaml from "js-yaml";
-import type { Benchmark, MetricPanel, ProviderResult } from "@/types/benchmark";
+import type {
+ Benchmark,
+ CellRankEntry,
+ MetricPanel,
+ ProviderResult,
+} from "@/types/benchmark";
import { Prometheus } from "@/lib/prometheus";
import { SpecSchema, type Spec } from "@/lib/spec-schema";
import { renderBenchmarkText } from "@/lib/bench-template";
@@ -455,6 +460,11 @@ async function specToBenchmark(
if (Object.keys(providers).length > 0) providersPerChain = providers;
}
+ // Exact per-cell rankings (chain × region) from the spec's single
+ // grouped matrix query. Failures are tolerated: badge/product
+ // surfaces fall back to the coarser bestPerChain path.
+ const cellRanks = !isFiltered ? await tryLoadCellRanks(spec) : undefined;
+
// Resolve {{p50:slug}} / {{best_name}} / {{count}} etc. placeholders
// against the freshly loaded numbers so editorial text (findings,
// seo_intro, faq) never drifts from the displayed data.
@@ -464,6 +474,7 @@ async function specToBenchmark(
bestPerChain,
worstPerChain,
providersPerChain,
+ cellRanks,
});
// Persist a snapshot of the runtime data so a future cold start
// during a Prom blackout can still render this bench. Only the
@@ -486,6 +497,102 @@ function activeFilterLabels(opts: BenchmarkFilters): Record {
return out;
}
+/**
+ * Run the spec's `rank_matrix_query` (one instant vector with a sample per
+ * (provider[, chain][, region])) and fold it into full per-cell rankings.
+ *
+ * Output keys are `|` with "all" standing in for an
+ * undeclared dimension. When BOTH dimensions are declared, marginal cells
+ * (`|all`, `all|`) are derived by averaging a provider's
+ * finest-cell values over the collapsed dimension — same semantics as the
+ * bench page's unscoped `avg(...)` headline queries.
+ *
+ * Samples whose provider label doesn't match a spec provider slug, or
+ * whose chain/region label isn't a declared dimension value, are dropped:
+ * the matrix is unfiltered PromQL, so stray series (retired providers,
+ * staging labels) must not leak into rankings.
+ */
+async function tryLoadCellRanks(
+ spec: Spec,
+): Promise | undefined> {
+ if (!spec.rank_matrix_query) return undefined;
+ const url = spec.prometheus?.url ?? process.env.PROMETHEUS_URL;
+ if (!url) return undefined;
+ try {
+ const prom = new Prometheus(url);
+ const res = await prom.query(spec.rank_matrix_query);
+ if (res.resultType !== "vector") return undefined;
+
+ const slugByLower = new Map(
+ spec.providers.map((p) => [p.slug.toLowerCase(), p.slug] as const),
+ );
+ const chainValues = new Set(
+ (spec.dimensions?.chain ?? []).map((c) => c.value).filter((v) => v !== "all"),
+ );
+ const regionValues = new Set(
+ (spec.dimensions?.region ?? []).map((r) => r.value).filter((v) => v !== "all"),
+ );
+
+ // key → provider slug → samples (averaged if the grouping left
+ // residual label splits, e.g. multiple replicas per region).
+ const acc = new Map>();
+ for (const sample of res.result) {
+ const slug = slugByLower.get((sample.metric.provider ?? "").toLowerCase());
+ if (!slug) continue;
+ const chain = chainValues.size > 0 ? sample.metric.chain : undefined;
+ const region = regionValues.size > 0 ? sample.metric.region : undefined;
+ if (chainValues.size > 0 && (!chain || !chainValues.has(chain))) continue;
+ if (regionValues.size > 0 && (!region || !regionValues.has(region))) continue;
+ const v = Number(sample.value[1]);
+ if (!Number.isFinite(v) || v <= 0) continue;
+ const key = `${chain ?? "all"}|${region ?? "all"}`;
+ const cell = acc.get(key) ?? new Map();
+ const vals = cell.get(slug) ?? [];
+ vals.push(v);
+ cell.set(slug, vals);
+ acc.set(key, cell);
+ }
+ if (acc.size === 0) return undefined;
+
+ const mean = (vals: number[]) =>
+ vals.reduce((a, b) => a + b, 0) / vals.length;
+ const sortCell = (cell: Map): CellRankEntry[] =>
+ [...cell.entries()]
+ .map(([slug, vals]) => ({ slug, p50: mean(vals) }))
+ .sort((a, b) =>
+ spec.higher_is_better ? b.p50 - a.p50 : a.p50 - b.p50,
+ );
+
+ const out: Record = {};
+ for (const [key, cell] of acc) out[key] = sortCell(cell);
+
+ // Marginals, only when both dimensions exist in the finest cells.
+ if (chainValues.size > 0 && regionValues.size > 0) {
+ const marginal = new Map>();
+ for (const [key, cell] of acc) {
+ const [chain, region] = key.split("|");
+ for (const [slug, vals] of cell) {
+ const v = mean(vals);
+ for (const mKey of [`${chain}|all`, `all|${region}`]) {
+ const mCell = marginal.get(mKey) ?? new Map();
+ const mVals = mCell.get(slug) ?? [];
+ mVals.push(v);
+ mCell.set(slug, mVals);
+ marginal.set(mKey, mCell);
+ }
+ }
+ }
+ for (const [key, cell] of marginal) out[key] = sortCell(cell);
+ }
+ return out;
+ } catch (e) {
+ console.warn(
+ `cellRanks skip: ${spec.slug} matrix query failed: ${e instanceof Error ? e.message : String(e)}`,
+ );
+ return undefined;
+ }
+}
+
/** Inject every active `