diff --git a/src/app/data-api/page.tsx b/src/app/data-api/page.tsx
new file mode 100644
index 00000000..22159832
--- /dev/null
+++ b/src/app/data-api/page.tsx
@@ -0,0 +1,306 @@
+import Link from "next/link";
+import { fetchDataApiSnapshot, GROUP_META, fmtDataValue } from "@/lib/data-api-stats";
+import { DataApiHubTabs } from "@/components/data-api-hub-tabs";
+import { pageMetadata } from "@/lib/page-metadata";
+import { safeJsonLd, buildBreadcrumbJsonLd } from "@/lib/jsonld";
+import { SITE } from "@/data/site";
+
+/**
+ * Hub landing page for the data API vertical: price feeds, token metadata,
+ * portfolio/wallet indexing, DEX coverage, and NFT data.
+ *
+ * Blob-only: loads the materialized bench blobs for each of the 9 data-API
+ * benchmarks. Never touches Prometheus at render time. Graceful degradation
+ * if blobs are missing — the page renders the "warming up" state with direct
+ * bench links, never a 404 or throw.
+ */
+
+const DESCRIPTION =
+ "Live benchmark rankings for crypto data APIs: price feed latency, token metadata coverage, wallet indexing freshness, DEX chain coverage, and NFT data quality.";
+
+export const metadata: import("next").Metadata = pageMetadata({
+ path: "/data-api",
+ title: "Best Crypto Data API 2026, ranked by benchmark",
+ description: DESCRIPTION,
+});
+
+export const revalidate = 60;
+
+const BENCH_SLUGS = [
+ "aggregator-head-lag",
+ "metadata-coverage",
+ "asset-registry-coverage",
+ "token-quote-coverage",
+ "indexing-freshness",
+ "portfolio-chain-coverage",
+ "wallet-labels-coverage",
+ "dex-network-coverage",
+ "nft-collection-metadata",
+] as const;
+
+export default async function DataApiHubPage() {
+ const snapshot = await fetchDataApiSnapshot();
+
+ const breadcrumbLd = {
+ "@context": "https://schema.org",
+ ...buildBreadcrumbJsonLd([
+ { name: "Home", item: SITE.url },
+ { name: "Data API benchmarks", item: `${SITE.url}/data-api` },
+ ]),
+ };
+
+ const itemListLd = {
+ "@context": "https://schema.org",
+ "@type": "ItemList",
+ name: "Crypto data API benchmarks by OpenChainBench",
+ description: DESCRIPTION,
+ numberOfItems: BENCH_SLUGS.length,
+ itemListElement: BENCH_SLUGS.map((slug, i) => ({
+ "@type": "ListItem",
+ position: i + 1,
+ name: slug,
+ url: `${SITE.url}/benchmarks/${slug}`,
+ })),
+ };
+
+ return (
+
+
+
+
+
+
+ Data APIs
+
+
+ Crypto data API benchmarks
+
+
+ Nine independent benchmarks across five categories: price feed head
+ lag, token metadata coverage, wallet indexing freshness, DEX network
+ coverage, and NFT data quality. Every number is measured live from
+ the same harness on the same schedule. No marketing claims, just probe
+ data.
+
+
+ {/* Bench spec badges */}
+
+ {BENCH_SLUGS.slice(0, 5).map((slug) => (
+
+
+ Bench
+
+ {slug}
+
+ ))}
+ {BENCH_SLUGS.length > 5 && (
+
+ +{BENCH_SLUGS.length - 5} more
+
+ )}
+
+
+
+ {snapshot ? (
+ <>
+ {/* KPI strip */}
+
+
+ {/* Group summary pills */}
+
+ {snapshot.groups.map(({ group, benches }) => {
+ const meta = GROUP_META[group];
+ const leader = benches[0]?.leader;
+ return (
+
+
+ {meta.shortLabel}
+ {leader && (
+
+ {leader.name}
+ {" "}
+
+ {fmtDataValue(leader.p50, benches[0].unit)}
+
+
+ )}
+
+ );
+ })}
+
+
+
+
+
+ All numbers are p50 values from the trailing 24h window unless noted.
+ Click any bench title for the full leaderboard with p90/p99, per-region
+ tabs, and time-series charts. Refresh interval 60s.
+
+ >
+ ) : (
+
+ )}
+
+
+
+ Methodology
+
+
+ Each benchmark uses an independent probe harness with its own
+ cadence and measurement axis. Price feed head lag captures
+ millisecond-precision timestamps at the probe node and at each
+ provider API, computing the wall-clock difference. Metadata and
+ coverage benches hit provider endpoints with canonical test tokens
+ or addresses, recording which fields are populated. Indexing
+ freshness measures the gap between a Base block confirmation and
+ the moment each wallet API returns the transaction. All harnesses
+ are open source on{" "}
+
+ GitHub
+
+ . Data released under{" "}
+
+ CC BY 4.0
+
+ .
+
+
+
+ );
+}
+
+function KpiCard({
+ label,
+ value,
+ accent,
+ tip,
+}: {
+ label: string;
+ value: string;
+ accent?: string;
+ tip?: string;
+}) {
+ return (
+
+
+ {accent && (
+
+ )}
+ {label}
+
+
+ {value}
+
+
+ );
+}
+
+function WarmingUp() {
+ return (
+
+
+ Data warming up
+
+
+ The cross-bench snapshot has not yet been published. Individual
+ benchmark pages are already live:
+
+
+ {BENCH_SLUGS.map((slug) => (
+
+
+ {slug}
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/data-api-bench-groups.tsx b/src/components/data-api-bench-groups.tsx
new file mode 100644
index 00000000..e57ae98b
--- /dev/null
+++ b/src/components/data-api-bench-groups.tsx
@@ -0,0 +1,271 @@
+"use client";
+
+import Link from "next/link";
+import { ProviderLogo } from "@/components/provider-logo";
+import {
+ GROUP_META,
+ fmtDataValue,
+ type DataApiGroupRow,
+ type DataApiBenchRow,
+ type RegionLeader,
+ type ChainLeader,
+} from "@/lib/data-api-stats";
+
+export function DataApiBenchGroups({ groups }: { groups: DataApiGroupRow[] }) {
+ return (
+
+ {groups.map(({ group, benches }) => {
+ const meta = GROUP_META[group];
+ return (
+
+
+
+
{meta.label}
+ {meta.description}
+
+
+
+
+
+
+
+ Benchmark
+ Providers
+ Leader
+ Runners-up
+ By region / chain
+
+
+
+ {benches.map((bench) => (
+
+ ))}
+
+
+
+
+
+ );
+ })}
+
+ );
+}
+
+function BenchRow({
+ bench,
+ accent,
+}: {
+ bench: DataApiBenchRow;
+ accent: string;
+}) {
+ const hasRegions = bench.regionLeaders.length > 0;
+ const hasChains = bench.chainLeaders.length > 0;
+ const showRegionChain = hasRegions || hasChains;
+
+ return (
+
+ {/* Bench title + number */}
+
+
+
+ {bench.shortTitle}
+
+
+
+ #{bench.number}
+
+ {bench.metric}
+
+
+
+
+ {/* Provider count */}
+
+
+ {bench.providerCount > 0 ? bench.providerCount : "..."}
+
+
+
+ {/* Leader */}
+
+ {bench.leader ? (
+
+
+
+
+ {bench.leader.name}
+
+
+ {fmtDataValue(bench.leader.p50, bench.unit)}
+
+
+
+ ) : (
+ warming up
+ )}
+
+
+ {/* Runners-up */}
+
+
+ {bench.runners.length > 0 ? (
+ bench.runners.map((r, i) => (
+
+
+ {i + 2}
+
+
+
+ {r.name}
+
+
+ {fmtDataValue(r.p50, bench.unit)}
+
+
+ ))
+ ) : (
+
...
+ )}
+
+
+
+ {/* Region / chain breakdown */}
+
+ {showRegionChain ? (
+
+ {hasRegions && (
+
+ )}
+ {hasChains && !hasRegions && (
+
+ )}
+
+ ) : (
+ global
+ )}
+
+
+ );
+}
+
+function RegionStrip({
+ leaders,
+ unit,
+}: {
+ leaders: RegionLeader[];
+ unit: DataApiBenchRow["unit"];
+}) {
+ const ORDER = ["us-east", "eu-west", "sgp"];
+ const sorted = [...leaders].sort(
+ (a, b) => ORDER.indexOf(a.region) - ORDER.indexOf(b.region),
+ );
+
+ return (
+
+ {sorted.map((l) => (
+
+
+ {l.region === "us-east" ? "US" : l.region === "eu-west" ? "EU" : "SGP"}
+
+
+
+ {l.providerName.split(" ")[0]}
+
+
+ {fmtDataValue(l.p50, unit)}
+
+
+ ))}
+
+ );
+}
+
+function ChainStrip({
+ leaders,
+ unit,
+}: {
+ leaders: ChainLeader[];
+ unit: DataApiBenchRow["unit"];
+}) {
+ return (
+
+ {leaders.map((l) => (
+
+
+ {l.label}
+
+
+
+ {l.providerName.split(" ")[0]}
+
+
+ {fmtDataValue(l.p50, unit)}
+
+
+ ))}
+
+ );
+}
+
+function Th({
+ children,
+ className = "",
+}: {
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/data-api-hub-tabs.tsx b/src/components/data-api-hub-tabs.tsx
new file mode 100644
index 00000000..5f811a86
--- /dev/null
+++ b/src/components/data-api-hub-tabs.tsx
@@ -0,0 +1,79 @@
+"use client";
+
+import { useState } from "react";
+import { DataApiBenchGroups } from "@/components/data-api-bench-groups";
+import { DataApiProvidersPivot } from "@/components/data-api-providers-pivot";
+import type { DataApiSnapshot } from "@/lib/data-api-stats";
+
+type Tab = "benchmarks" | "providers";
+
+export function DataApiHubTabs({ snapshot }: { snapshot: DataApiSnapshot }) {
+ const [tab, setTab] = useState("benchmarks");
+
+ return (
+ <>
+
+ setTab("benchmarks")}
+ count={snapshot.totals.benchCount}
+ >
+ By benchmark
+
+ setTab("providers")}
+ count={snapshot.totals.uniqueProviders}
+ >
+ By provider
+
+
+
+ {tab === "benchmarks" && (
+
+ )}
+ {tab === "providers" && (
+
+ )}
+ >
+ );
+}
+
+function TabButton({
+ children,
+ active,
+ count,
+ onClick,
+}: {
+ children: React.ReactNode;
+ active: boolean;
+ count: number;
+ onClick: () => void;
+}) {
+ return (
+
+ {children}
+
+ {count}
+
+
+ );
+}
diff --git a/src/components/data-api-providers-pivot.tsx b/src/components/data-api-providers-pivot.tsx
new file mode 100644
index 00000000..37e391ea
--- /dev/null
+++ b/src/components/data-api-providers-pivot.tsx
@@ -0,0 +1,318 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import Link from "next/link";
+import { ProviderLogo } from "@/components/provider-logo";
+import {
+ GROUP_ORDER,
+ GROUP_META,
+ fmtDataValue,
+ type DataApiProviderPivotRow,
+ type DataApiGroup,
+} from "@/lib/data-api-stats";
+
+type SortKey = "groupCount" | DataApiGroup;
+
+export function DataApiProvidersPivot({
+ rows,
+ groupCount,
+}: {
+ rows: DataApiProviderPivotRow[];
+ groupCount: number;
+}) {
+ const [sortKey, setSortKey] = useState("groupCount");
+ const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
+ const [q, setQ] = useState("");
+
+ const filtered = useMemo(() => {
+ const needle = q.trim().toLowerCase();
+ const base = needle
+ ? rows.filter(
+ (r) =>
+ r.name.toLowerCase().includes(needle) ||
+ r.slug.toLowerCase().includes(needle),
+ )
+ : rows;
+
+ return [...base].sort((a, b) => {
+ const factor = sortDir === "desc" ? -1 : 1;
+
+ if (sortKey === "groupCount") {
+ if (b.groupCount !== a.groupCount)
+ return factor * (b.groupCount - a.groupCount);
+ // tie-break: better avg rank
+ const avgA = a.cells.reduce((s, c) => s + c.rank, 0) / (a.cells.length || 1);
+ const avgB = b.cells.reduce((s, c) => s + c.rank, 0) / (b.cells.length || 1);
+ return avgA - avgB;
+ }
+
+ // Sort by a specific group's best rank
+ const cellA = a.groups[sortKey];
+ const cellB = b.groups[sortKey];
+ if (!cellA && !cellB) return 0;
+ if (!cellA) return 1;
+ if (!cellB) return -1;
+ // rank: lower = better always
+ return factor * (cellA.bestRank - cellB.bestRank);
+ });
+ }, [rows, sortKey, sortDir, q]);
+
+ const setSort = (k: SortKey) => {
+ if (k === sortKey) setSortDir((d) => (d === "desc" ? "asc" : "desc"));
+ else {
+ setSortKey(k);
+ setSortDir("desc");
+ }
+ };
+
+ const visibleGroups = GROUP_ORDER.filter(
+ (g) => rows.some((r) => r.groups[g]),
+ );
+
+ return (
+
+
+
+ {filtered.length} of {rows.length} providers across {groupCount} benchmark groups
+
+
setQ(e.target.value)}
+ placeholder="Search provider..."
+ className="text-[12.5px] px-3 py-1.5 rounded-md border border-ink/15 bg-paper focus:outline-none focus:ring-2 focus:ring-violet-500/30 min-w-[180px]"
+ />
+
+
+
+
+
+
+ #
+ Provider
+ setSort("groupCount")}
+ >
+ Coverage
+
+ {visibleGroups.map((g) => (
+ setSort(g)}
+ accent={GROUP_META[g].accent}
+ >
+ {GROUP_META[g].shortLabel}
+
+ ))}
+
+
+
+ {filtered.map((row, idx) => (
+
+ ))}
+
+
+
+
+ {filtered.length === 0 && (
+
+ No providers match "{q}"
+
+ )}
+
+ );
+}
+
+function ProviderRow({
+ row,
+ rank,
+ groups,
+}: {
+ row: DataApiProviderPivotRow;
+ rank: number;
+ groups: DataApiGroup[];
+}) {
+ return (
+
+
+
+ {rank}
+
+
+
+
+
+ {row.name}
+
+
+
+ {/* Coverage column */}
+
+
+
{row.groupCount}
+
/ {groups.length}
+
+ {groups.map((g) => (
+
+ ))}
+
+
+
+
+ {/* One cell per group */}
+ {groups.map((g) => (
+
+
+
+ ))}
+
+ );
+}
+
+function GroupCell({
+ cell,
+ group,
+}: {
+ cell: DataApiProviderPivotRow["groups"][DataApiGroup];
+ group: DataApiGroup;
+}) {
+ if (!cell) {
+ return — ;
+ }
+
+ const { bestRank, bestCell } = cell;
+ const accent = GROUP_META[group].accent;
+
+ // Color intensity by rank
+ const bg =
+ bestRank === 1
+ ? `${accent}22`
+ : bestRank <= 3
+ ? `${accent}11`
+ : "transparent";
+
+ const textColor =
+ bestRank === 1
+ ? accent
+ : bestRank <= 3
+ ? accent
+ : "var(--color-ink-soft)";
+
+ return (
+
+
+
+
+ {fmtDataValue(bestCell.p50, bestCell.unit)}
+
+
+
+ {bestCell.benchShortTitle.replace("coverage", "cov.").replace("freshness", "fresh.")}
+
+
+ );
+}
+
+function RankBadge({ rank, accent }: { rank: number; accent: string }) {
+ const bg = rank === 1 ? accent : "transparent";
+ const border = rank <= 3 ? `1px solid ${accent}66` : "1px solid transparent";
+ const color =
+ rank === 1 ? "#fff" : rank <= 3 ? accent : "var(--color-ink-faint)";
+
+ return (
+
+ {rank}
+
+ );
+}
+
+function Th({
+ children,
+ className = "",
+}: {
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+function ThSort({
+ children,
+ active,
+ dir,
+ onClick,
+ accent,
+}: {
+ children: React.ReactNode;
+ active: boolean;
+ dir: "asc" | "desc";
+ onClick: () => void;
+ accent?: string;
+}) {
+ return (
+
+
+ {children}
+ {active ? (dir === "desc" ? "▼" : "▲") : "⇅"}
+
+
+ );
+}
diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx
index aef5dbd8..032f8ac5 100644
--- a/src/components/site-header.tsx
+++ b/src/components/site-header.tsx
@@ -36,6 +36,11 @@ const NAV: NavItem[] = [
label: "Benchmarks",
match: (p) => p === "/benchmarks" || p.startsWith("/benchmarks/"),
},
+ {
+ href: "/data-api",
+ label: "Data APIs",
+ match: (p) => p === "/data-api" || p.startsWith("/data-api/"),
+ },
{
href: "/products",
label: "Products",
diff --git a/src/lib/data-api-stats.ts b/src/lib/data-api-stats.ts
new file mode 100644
index 00000000..f8a0a084
--- /dev/null
+++ b/src/lib/data-api-stats.ts
@@ -0,0 +1,417 @@
+/**
+ * Server-side helper for the /data-api hub page. Loads bench blobs for
+ * every "data API" benchmark (price feeds, token metadata, portfolio/wallet,
+ * DEX coverage, NFT data) and assembles two views:
+ *
+ * 1. By Benchmark — groups benches by use-case, each bench has its leader,
+ * runners-up, per-region leaders (when the bench has region data), and
+ * per-chain leaders (when the bench has bestPerChain populated).
+ *
+ * 2. By Provider — cross-bench pivot: one row per unique provider, one cell
+ * per group, showing the provider's best rank + value in that group.
+ *
+ * Blob-only: reads from the CDN bench blobs published by the materialize
+ * worker. Never touches Prometheus at render time. Graceful degradation:
+ * missing/empty blobs render as null cells (never a throw).
+ */
+
+import { unstable_cache } from "next/cache";
+import { loadBenchFromBlob } from "@/lib/bench-blob";
+import type { Benchmark, ProviderResult } from "@/types/benchmark";
+
+export type DataApiGroup =
+ | "price-feeds"
+ | "token-metadata"
+ | "portfolio-wallet"
+ | "dex-coverage"
+ | "nft-data";
+
+export const GROUP_ORDER: readonly DataApiGroup[] = [
+ "price-feeds",
+ "token-metadata",
+ "portfolio-wallet",
+ "dex-coverage",
+ "nft-data",
+];
+
+export const GROUP_META: Record<
+ DataApiGroup,
+ { label: string; shortLabel: string; accent: string; description: string }
+> = {
+ "price-feeds": {
+ label: "Price & Market Data",
+ shortLabel: "Price Feeds",
+ accent: "#f59e0b",
+ description: "Real-time price feed latency from on-chain event to API emission.",
+ },
+ "token-metadata": {
+ label: "Token Metadata",
+ shortLabel: "Token Data",
+ accent: "#8b5cf6",
+ description: "Metadata coverage, asset registry depth, and quote routing capacity.",
+ },
+ "portfolio-wallet": {
+ label: "Portfolio & Wallet",
+ shortLabel: "Portfolio",
+ accent: "#10b981",
+ description: "Wallet indexing freshness, portfolio chain coverage, and label accuracy.",
+ },
+ "dex-coverage": {
+ label: "DEX Coverage",
+ shortLabel: "DEX",
+ accent: "#0ea5e9",
+ description: "Number of blockchains where each DEX indexer tracks pools and swap volumes.",
+ },
+ "nft-data": {
+ label: "NFT Data",
+ shortLabel: "NFT",
+ accent: "#f43f5e",
+ description: "Collection metadata field coverage across blue-chip NFT collections.",
+ },
+};
+
+/** Each bench's group. Adding a bench to the hub = one line here. */
+const BENCH_GROUP: Record = {
+ "aggregator-head-lag": "price-feeds",
+ "metadata-coverage": "token-metadata",
+ "asset-registry-coverage": "token-metadata",
+ "token-quote-coverage": "token-metadata",
+ "indexing-freshness": "portfolio-wallet",
+ "portfolio-chain-coverage": "portfolio-wallet",
+ "wallet-labels-coverage": "portfolio-wallet",
+ "dex-network-coverage": "dex-coverage",
+ "nft-collection-metadata": "nft-data",
+};
+
+const BENCH_SLUGS = Object.keys(BENCH_GROUP);
+
+/** Short display titles for table rows (trimmed from the full spec title). */
+const BENCH_SHORT_TITLE: Record = {
+ "aggregator-head-lag": "Price feed head lag",
+ "metadata-coverage": "Token metadata coverage",
+ "asset-registry-coverage": "Asset registry chains",
+ "token-quote-coverage": "Token quote coverage",
+ "indexing-freshness": "Wallet indexing freshness",
+ "portfolio-chain-coverage": "Portfolio chain coverage",
+ "wallet-labels-coverage": "Wallet label coverage",
+ "dex-network-coverage": "DEX network coverage",
+ "nft-collection-metadata": "NFT metadata coverage",
+};
+
+const CHAIN_LABELS: Record = {
+ base: "Base",
+ bnb: "BNB",
+ solana: "SOL",
+ robinhood: "RH",
+ ethereum: "ETH",
+};
+
+export type RegionLeader = {
+ region: string;
+ label: string;
+ providerSlug: string;
+ providerName: string;
+ p50: number;
+};
+
+export type ChainLeader = {
+ chain: string;
+ label: string;
+ providerSlug: string;
+ providerName: string;
+ p50: number;
+};
+
+export type DataApiBenchRow = {
+ slug: string;
+ number: string;
+ title: string;
+ shortTitle: string;
+ metric: string;
+ unit: Benchmark["unit"];
+ higherIsBetter: boolean;
+ leader: { slug: string; name: string; p50: number } | null;
+ runners: { slug: string; name: string; p50: number }[];
+ providerCount: number;
+ regionLeaders: RegionLeader[];
+ chainLeaders: ChainLeader[];
+ updatedAt: string | null;
+};
+
+export type DataApiGroupRow = {
+ group: DataApiGroup;
+ benches: DataApiBenchRow[];
+};
+
+export type DataApiProviderCell = {
+ benchSlug: string;
+ benchShortTitle: string;
+ group: DataApiGroup;
+ rank: number;
+ p50: number;
+ unit: Benchmark["unit"];
+ higherIsBetter: boolean;
+};
+
+export type DataApiProviderPivotRow = {
+ slug: string;
+ name: string;
+ cells: DataApiProviderCell[];
+ groups: Partial<
+ Record
+ >;
+ groupCount: number;
+};
+
+export type DataApiSnapshot = {
+ groups: DataApiGroupRow[];
+ providers: DataApiProviderPivotRow[];
+ totals: {
+ uniqueProviders: number;
+ benchCount: number;
+ groupCount: number;
+ headlinePriceFeed: { name: string; value: string } | null;
+ headlineIndexing: { name: string; value: string } | null;
+ };
+ generatedAt: string;
+};
+
+function liveResults(bench: Benchmark): ProviderResult[] {
+ return bench.results.filter(
+ (r) =>
+ r.availability !== "unavailable" &&
+ !r.unresponsive &&
+ r.ms.p50 > 0 &&
+ r.dataConfidence !== "insufficient",
+ );
+}
+
+function sortedResults(
+ results: ProviderResult[],
+ higherIsBetter: boolean,
+): ProviderResult[] {
+ return [...results].sort((a, b) =>
+ higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50,
+ );
+}
+
+function computeRegionLeaders(bench: Benchmark): RegionLeader[] {
+ const live = liveResults(bench);
+ const { regions } = bench.extras;
+ if (!regions || Object.keys(regions).length === 0) return [];
+
+ const map = new Map();
+
+ for (const provider of live) {
+ const pts = regions[provider.slug] ?? [];
+ for (const pt of pts) {
+ if (pt.region === "global") continue;
+ const normalized =
+ pt.region === "ap-southeast" ? "sgp" : pt.region;
+ const label =
+ normalized === "sgp"
+ ? "Singapore"
+ : normalized === "us-east"
+ ? "US East"
+ : "EU West";
+
+ const current = map.get(normalized);
+ const isBetter = bench.higherIsBetter
+ ? pt.p50 > (current?.p50 ?? -Infinity)
+ : pt.p50 < (current?.p50 ?? Infinity);
+
+ if (isBetter || !current) {
+ map.set(normalized, {
+ region: normalized,
+ label,
+ providerSlug: provider.slug,
+ providerName: provider.name,
+ p50: pt.p50,
+ });
+ }
+ }
+ }
+
+ return (["us-east", "eu-west", "sgp"] as const)
+ .filter((r) => map.has(r))
+ .map((r) => map.get(r)!);
+}
+
+function computeChainLeaders(bench: Benchmark): ChainLeader[] {
+ if (!bench.bestPerChain) return [];
+ return Object.entries(bench.bestPerChain)
+ .filter(([, r]) => r.availability !== "unavailable" && !r.unresponsive && r.ms.p50 > 0)
+ .map(([chain, r]) => ({
+ chain,
+ label: CHAIN_LABELS[chain] ?? chain,
+ providerSlug: r.slug,
+ providerName: r.name,
+ p50: r.ms.p50,
+ }))
+ .sort((a, b) => a.label.localeCompare(b.label));
+}
+
+function toBenchRow(slug: string, bench: Benchmark): DataApiBenchRow {
+ const live = sortedResults(liveResults(bench), bench.higherIsBetter);
+ return {
+ slug,
+ number: bench.number,
+ title: bench.title,
+ shortTitle: BENCH_SHORT_TITLE[slug] ?? bench.title,
+ metric: bench.metric,
+ unit: bench.unit,
+ higherIsBetter: bench.higherIsBetter,
+ leader: live[0]
+ ? { slug: live[0].slug, name: live[0].name, p50: live[0].ms.p50 }
+ : null,
+ runners: live.slice(1, 4).map((r) => ({
+ slug: r.slug,
+ name: r.name,
+ p50: r.ms.p50,
+ })),
+ providerCount: live.length,
+ regionLeaders: computeRegionLeaders(bench),
+ chainLeaders: computeChainLeaders(bench),
+ updatedAt: bench.lastRunAt ?? null,
+ };
+}
+
+export function fmtDataValue(v: number, unit: Benchmark["unit"]): string {
+ if (!Number.isFinite(v)) return "...";
+ switch (unit) {
+ case "pct":
+ return `${v.toFixed(1)}%`;
+ case "count":
+ return String(Math.round(v));
+ case "s":
+ case "sec":
+ return v < 1
+ ? `${(v * 1000).toFixed(0)} ms`
+ : `${v.toFixed(2)} s`;
+ case "ms":
+ return `${Math.round(v)} ms`;
+ case "bps":
+ case "bp":
+ return `${v.toFixed(0)} bps`;
+ default:
+ return String(Math.round(v));
+ }
+}
+
+async function buildSnapshot(): Promise {
+ const settled = await Promise.allSettled(
+ BENCH_SLUGS.map((s) => loadBenchFromBlob(s)),
+ );
+
+ const loaded: [string, Benchmark][] = BENCH_SLUGS.reduce<
+ [string, Benchmark][]
+ >((acc, slug, i) => {
+ const r = settled[i];
+ if (r.status === "fulfilled" && r.value) acc.push([slug, r.value]);
+ return acc;
+ }, []);
+
+ if (loaded.length === 0) return null;
+
+ // Build group rows
+ const groupBenches: Record = {
+ "price-feeds": [],
+ "token-metadata": [],
+ "portfolio-wallet": [],
+ "dex-coverage": [],
+ "nft-data": [],
+ };
+
+ for (const [slug, bench] of loaded) {
+ const group = BENCH_GROUP[slug];
+ if (!group) continue;
+ groupBenches[group].push(toBenchRow(slug, bench));
+ }
+
+ // Build provider pivot
+ type PEntry = { name: string; cells: DataApiProviderCell[] };
+ const providerMap = new Map();
+
+ for (const [slug, bench] of loaded) {
+ const group = BENCH_GROUP[slug];
+ if (!group) continue;
+ const live = sortedResults(liveResults(bench), bench.higherIsBetter);
+ for (const [idx, r] of live.entries()) {
+ if (!providerMap.has(r.slug)) {
+ providerMap.set(r.slug, { name: r.name, cells: [] });
+ }
+ providerMap.get(r.slug)!.cells.push({
+ benchSlug: slug,
+ benchShortTitle: BENCH_SHORT_TITLE[slug] ?? slug,
+ group,
+ rank: idx + 1,
+ p50: r.ms.p50,
+ unit: bench.unit,
+ higherIsBetter: bench.higherIsBetter,
+ });
+ }
+ }
+
+ const providerRows: DataApiProviderPivotRow[] = [];
+ for (const [slug, { name, cells }] of providerMap.entries()) {
+ const groups: DataApiProviderPivotRow["groups"] = {};
+ for (const cell of cells) {
+ const cur = groups[cell.group];
+ if (!cur || cell.rank < cur.bestRank) {
+ groups[cell.group] = { bestRank: cell.rank, bestCell: cell };
+ }
+ }
+ providerRows.push({
+ slug,
+ name,
+ cells,
+ groups,
+ groupCount: Object.keys(groups).length,
+ });
+ }
+
+ providerRows.sort((a, b) => {
+ if (b.groupCount !== a.groupCount) return b.groupCount - a.groupCount;
+ const avgA = a.cells.reduce((s, c) => s + c.rank, 0) / (a.cells.length || 1);
+ const avgB = b.cells.reduce((s, c) => s + c.rank, 0) / (b.cells.length || 1);
+ return avgA - avgB;
+ });
+
+ // Headline stats for KPI strip
+ const headLagEntry = loaded.find(([s]) => s === "aggregator-head-lag");
+ const indexingEntry = loaded.find(([s]) => s === "indexing-freshness");
+
+ function headlineStat(
+ entry: [string, Benchmark] | undefined,
+ ): { name: string; value: string } | null {
+ if (!entry) return null;
+ const [, bench] = entry;
+ const best = sortedResults(liveResults(bench), bench.higherIsBetter)[0];
+ if (!best) return null;
+ return { name: best.name, value: fmtDataValue(best.ms.p50, bench.unit) };
+ }
+
+ const groups: DataApiGroupRow[] = GROUP_ORDER.map((g) => ({
+ group: g,
+ benches: groupBenches[g],
+ })).filter((g) => g.benches.length > 0);
+
+ return {
+ groups,
+ providers: providerRows,
+ totals: {
+ uniqueProviders: providerMap.size,
+ benchCount: loaded.length,
+ groupCount: groups.length,
+ headlinePriceFeed: headlineStat(headLagEntry),
+ headlineIndexing: headlineStat(indexingEntry),
+ },
+ generatedAt: new Date().toISOString(),
+ };
+}
+
+export const fetchDataApiSnapshot = unstable_cache(
+ buildSnapshot,
+ ["data-api-cohort"],
+ { revalidate: 60, tags: ["data-api-cohort"] },
+);
diff --git a/src/lib/pm-stats.ts b/src/lib/pm-stats.ts
index bfaff899..26dd8f60 100644
--- a/src/lib/pm-stats.ts
+++ b/src/lib/pm-stats.ts
@@ -74,10 +74,11 @@ type DataFeedSeed = {
};
const PM_VENUES: VenueSeed[] = [
- { slug: "polymarket", name: "Polymarket", type: "onchain", chain: "polygon" },
- { slug: "kalshi", name: "Kalshi", type: "offchain" },
- { slug: "limitless", name: "Limitless", type: "onchain", chain: "base" },
- { slug: "myriad", name: "Myriad", type: "onchain", chain: "abstract" },
+ { slug: "polymarket", name: "Polymarket", type: "onchain", chain: "polygon" },
+ { slug: "polymarket-us", name: "Polymarket US", type: "offchain" },
+ { slug: "kalshi", name: "Kalshi", type: "offchain" },
+ { slug: "limitless", name: "Limitless", type: "onchain", chain: "base" },
+ { slug: "myriad", name: "Myriad", type: "onchain", chain: "abstract" },
];
const PM_DATA_FEEDS: DataFeedSeed[] = [