From 6c6aab0c5916efe313a690e88a54722214c03206 Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 15:28:56 +0200
Subject: [PATCH 01/14] feat(pm): add polymarket-us to PM_VENUES seed list
---
src/lib/pm-stats.ts | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
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[] = [
From e280e0e928c13029bbaaa03784048ea2f5eeb776 Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 15:30:06 +0200
Subject: [PATCH 02/14] =?UTF-8?q?feat:=20add=20/data-api=20hub=20=E2=80=94?=
=?UTF-8?q?=209=20benches,=20by-benchmark=20+=20by-provider=20views?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/data-api/page.tsx | 306 ++++++++++++++
src/components/data-api-bench-groups.tsx | 271 +++++++++++++
src/components/data-api-hub-tabs.tsx | 79 ++++
src/components/data-api-providers-pivot.tsx | 318 +++++++++++++++
src/components/site-header.tsx | 5 +
src/lib/data-api-stats.ts | 417 ++++++++++++++++++++
6 files changed, 1396 insertions(+)
create mode 100644 src/app/data-api/page.tsx
create mode 100644 src/components/data-api-bench-groups.tsx
create mode 100644 src/components/data-api-hub-tabs.tsx
create mode 100644 src/components/data-api-providers-pivot.tsx
create mode 100644 src/lib/data-api-stats.ts
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"] },
+);
From be95f231d099136fc7b5681c8f103cdd573a67b5 Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 15:39:26 +0200
Subject: [PATCH 03/14] debug: catch and expose render errors in share-card
route
---
.../benchmarks/[slug]/share-card/route.tsx | 33 ++++++++++++-------
1 file changed, 21 insertions(+), 12 deletions(-)
diff --git a/src/app/benchmarks/[slug]/share-card/route.tsx b/src/app/benchmarks/[slug]/share-card/route.tsx
index 08a3a50c..e84913cc 100644
--- a/src/app/benchmarks/[slug]/share-card/route.tsx
+++ b/src/app/benchmarks/[slug]/share-card/route.tsx
@@ -672,18 +672,27 @@ export async function GET(
const colors = buildProviderColors(benchmark.results);
- switch (template) {
- case "snapshot":
- return renderSnapshot(filteredSafe, colors, chainLabel);
- case "headline":
- return renderHeadline(benchmark, colors, headlineProvider, chainLabel);
- case "compare":
- return renderCompare(benchmark, colors, compareA, compareB, chainLabel);
- case "leaderboard":
- return renderLeaderboard(benchmark, colors, chainLabel);
- case "ranking":
- default:
- return renderRanking(benchmark, colors, chainLabel);
+ try {
+ switch (template) {
+ case "snapshot":
+ return await renderSnapshot(filteredSafe, colors, chainLabel);
+ case "headline":
+ return await renderHeadline(benchmark, colors, headlineProvider, chainLabel);
+ case "compare":
+ return await renderCompare(benchmark, colors, compareA, compareB, chainLabel);
+ case "leaderboard":
+ return await renderLeaderboard(benchmark, colors, chainLabel);
+ case "ranking":
+ default:
+ return await renderRanking(benchmark, colors, chainLabel);
+ }
+ } catch (err) {
+ const msg = err instanceof Error ? `${err.message}\n${err.stack}` : String(err);
+ console.error(`[share-card] ${slug} template=${template} ERROR:`, msg);
+ return new Response(`render_error: ${err instanceof Error ? err.message : String(err)}`, {
+ status: 500,
+ headers: { "content-type": "text/plain" },
+ });
}
}
From da63fc0c98d4e507dfb9775f184b654003ba400f Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 15:45:53 +0200
Subject: [PATCH 04/14] fix: allow all long-range tabs in panel views, map
archive field by panel
- Remove panelBlocks restriction: 180D/ALL now clickable even when a panel
(users/volume/fees) is active, since archive data covers all windows
- pickPanel() falls through to longRangeSeries when panelLazy90d/1y is
empty (Prom has no 90d panel series), showing archive data instead of
an empty chart
- hlLongRangeSeries maps the correct archive field (users/vol/fees) based
on activePanelId so switching panels also updates the long-range chart
- Add validator-yield-hyperliquid bench (115): all 34 HL validators as
providers, ranked by net yield bps
---
benchmarks/validator-yield-hyperliquid.yml | 447 +++++++++++++++++++++
src/components/benchmark-body.tsx | 24 +-
src/components/time-series-chart/index.tsx | 28 +-
3 files changed, 479 insertions(+), 20 deletions(-)
create mode 100644 benchmarks/validator-yield-hyperliquid.yml
diff --git a/benchmarks/validator-yield-hyperliquid.yml b/benchmarks/validator-yield-hyperliquid.yml
new file mode 100644
index 00000000..0164db43
--- /dev/null
+++ b/benchmarks/validator-yield-hyperliquid.yml
@@ -0,0 +1,447 @@
+# OpenChainBench. Bench № 115
+
+slug: validator-yield-hyperliquid
+number: "115"
+title: Hyperliquid validator leaderboard — live net APR, uptime, commission
+seo_title: "Best Hyperliquid validators 2026: yield, commission, uptime ranked"
+seo_description: "Live Hyperliquid validator leaderboard ranked by net APR. All 34 validators tracked: net yield in bps, commission rate, uptime %, delegated stake in USD. Updated every 5 minutes from /info validatorSummaries."
+subtitle: Per-validator net yield (predictedApr.day × uptime) in basis points across all 34 Hyperliquid validators. Ranked by net APR; commission and uptime shown as secondary columns.
+category: Blockchains
+status: live
+metric: Net yield
+unit: bps
+higher_is_better: true
+
+disclaimer: |
+ Honest scope. (1) `net_yield = predictedApr.day × uptimeFraction` from the Hyperliquid `/info validatorSummaries` endpoint. (2) No MEV layer exists at the validator level on Hyperliquid — the centralised sequencer captures order-flow value upstream. MEV is structurally absent from all figures. (3) Jailed validators (currently 6) land in the dataset with `predictedApr=0` and `net_yield=0`; they rank at the bottom. (4) APRs across the active set are tightly clustered (~200–225 bps) because Hyperliquid distributes rewards evenly across non-jailed validators — commission rate is the primary differentiator for delegators.
+
+seo_intro: |
+ Hyperliquid runs a permissioned validator set of ~30 active nodes. Unlike
+ Solana's ~3000-validator market where yield spreads across hundreds of bps,
+ every non-jailed Hyperliquid validator earns the same gross APR from the
+ protocol — the only differentiation is commission rate, uptime, and jail
+ risk. This page surfaces the full active set, ranked by net yield
+ (gross APR after commission and downtime drag), so a delegator can see
+ exactly which operators take the smallest cut and stay online.
+
+ The data updates every 5 minutes from the Hyperliquid `/info
+ validatorSummaries` endpoint. Net yield is `predictedApr.day ×
+ uptimeFraction`, expressed in basis points (1% = 100 bps).
+
+abstract: |
+ Every 5 minutes, the validator-yield harness POSTs `{"type":
+ "validatorSummaries"}` to `api.hyperliquid.xyz/info` and reads
+ `predictedApr.day` × `stats[day].uptimeFraction` per validator. Jailed
+ validators receive `net_yield_bps = 0` regardless of their published APR.
+ Commission is taken from the `commission` field (decimal fraction, e.g.
+ `0.04` = 4% = 400 bps). Delegated stake is the `stake` field divided by
+ `1e8` (HYPE token has 8 decimal places) then multiplied by the live HYPE
+ oracle price from `metaAndAssetCtxs`. All metrics land in Prometheus under
+ `ocb_validator_net_yield_bps{chain="hyperliquid", validator=,
+ name=}`.
+
+methodology:
+ - "Source: `POST api.hyperliquid.xyz/info {type: validatorSummaries}`. Fields used: `predictedApr` (day window), `stats[day].uptimeFraction`, `commission`, `stake` (in HYPE-wei, divide by 1e8), `isJailed`, `isActive`."
+ - "Net yield formula: `net_yield_bps = predictedApr.day × uptimeFraction × 10000`. Jailed validators are forced to 0 regardless of published APR."
+ - "Commission is a decimal fraction from the API (0.04 = 4%). Displayed in bps (0.04 × 10000 = 400 bps). Lower commission = better for delegators."
+ - "Stake USD: `(stake / 1e8) × HYPE oracle price`. HYPE price from `metaAndAssetCtxs` `oraclePx` (fallback: markPx, midPx)."
+ - "Cadence: 5-minute scrape. HL validator stats move on sub-hour timescales (consensus window = day, but jail/unjail is near-instant), so 5-minute polling is sufficient."
+ - "The bench includes all 34 validators currently returned by validatorSummaries, including jailed and near-zero-stake nodes, for complete transparency. Jailed nodes land at 0 bps yield."
+
+findings:
+ - "Commission is the only meaningful differentiator among healthy Hyperliquid validators: all active non-jailed validators earn the same gross APR (~225 bps as of this snapshot). A delegator choosing between them is effectively choosing a commission rate."
+ - "Zero-commission validators (infinitefield.xyz, HyperStake, CMI, Flowdex) lead the net yield ranking by construction — they take no cut from delegators."
+ - "The four Hyper Foundation validators (1–4) collectively hold ~$10B+ in staked HYPE and charge 3% commission (300 bps). Foundation-5 added later at the same terms."
+ - "Anchorage By Figment charges 10% commission (1000 bps), the highest among active non-jailed validators, which reduces delegator net yield by ~20 bps relative to zero-commission nodes."
+ - "6 validators are currently jailed (Figment, Falcon Capital, Asymmetric Research, HyperCN X hlscan, Red Pond, cp0x by STAKR.space). They earn 0 yield and their delegated stake is minimal (<10k HYPE each)."
+
+faq:
+ - q: "Why are all active Hyperliquid validators at nearly the same APR?"
+ a: "Hyperliquid distributes staking rewards evenly across the active (non-jailed) validator set. There is no MEV layer at the validator level, and no per-validator block-production variability like on Solana. The gross APR is set by the protocol reward pool divided equally. Commission rate is the only variable that affects delegator yield, which is why the net yield ranking is almost a direct inversion of the commission ranking."
+ - q: "What does 'jailed' mean on Hyperliquid?"
+ a: "Jailed validators have been penalised by the consensus mechanism for a fault (e.g., double-sign, downtime threshold breach). A jailed validator earns no rewards and its delegators earn nothing. The validator can be unjailed after a cooldown period. This bench surfaces jailed validators explicitly rather than filtering them, so delegators can see which operators have a jail history."
+ - q: "Why does Hyperliquid yield (~200 bps) look lower than Solana (~560 bps)?"
+ a: "Three structural reasons. (1) Hyperliquid pays staking rewards in HYPE only, with no inflation subsidy analogous to Solana's ~5% protocol issuance. (2) No MEV tips exist at the validator level. (3) The reward pool is split evenly across ~30 validators, giving a flat ~2% APR. A delegator choosing Hyperliquid is accepting lower yield in exchange for simpler, more predictable returns with no MEV variance."
+ - q: "How often does the data update?"
+ a: "Every 5 minutes. Validator APR on Hyperliquid moves on consensus window timescales (the day stat window refreshes continuously), so 5-minute polling captures any commission changes, jail events, or APR shifts within a single business-hour interval."
+
+source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/validator-yield
+
+prometheus:
+ window: 24h
+ expected_freshness_seconds: 600
+
+providers:
+ - slug: hyper-foundation-3
+ name: "Hyper Foundation 3"
+ tag: "3% commission · 55.7M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 3% (300 bps). Hyper Foundation validator."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyper Foundation 3"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Hyper Foundation 3"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Hyper Foundation 3"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Hyper Foundation 3"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyper Foundation 3"}[24h])
+
+ - slug: hyper-foundation-2
+ name: "Hyper Foundation 2"
+ tag: "3% commission · 55.1M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 3% (300 bps). Hyper Foundation validator."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyper Foundation 2"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Hyper Foundation 2"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Hyper Foundation 2"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Hyper Foundation 2"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyper Foundation 2"}[24h])
+
+ - slug: hyper-foundation-1
+ name: "Hyper Foundation 1"
+ tag: "3% commission · 52.4M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 3% (300 bps). Hyper Foundation validator."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyper Foundation 1"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Hyper Foundation 1"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Hyper Foundation 1"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Hyper Foundation 1"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyper Foundation 1"}[24h])
+
+ - slug: hyper-foundation-4
+ name: "Hyper Foundation 4"
+ tag: "3% commission · 34.7M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 3% (300 bps). Hyper Foundation validator."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyper Foundation 4"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Hyper Foundation 4"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Hyper Foundation 4"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Hyper Foundation 4"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyper Foundation 4"}[24h])
+
+ - slug: anchorage-by-figment
+ name: "Anchorage By Figment"
+ tag: "10% commission · 29.6M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 10% (1000 bps) — highest among active non-jailed validators."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Anchorage By Figment"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Anchorage By Figment"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Anchorage By Figment"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Anchorage By Figment"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Anchorage By Figment"}[24h])
+
+ - slug: hypurrscanning
+ name: "Hypurrscanning"
+ tag: "1% commission · 23.3M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 1% (100 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hypurrscanning"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Hypurrscanning"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Hypurrscanning"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Hypurrscanning"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hypurrscanning"}[24h])
+
+ - slug: hyperliquid-strategies-x-unit
+ name: "Hyperliquid Strategies x Unit"
+ tag: "2% commission · 22.6M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 2% (200 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyperliquid Strategies x Unit"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Hyperliquid Strategies x Unit"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Hyperliquid Strategies x Unit"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Hyperliquid Strategies x Unit"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyperliquid Strategies x Unit"}[24h])
+
+ - slug: nansen-x-hypurrcollective
+ name: "Nansen x HypurrCollective"
+ tag: "2% commission · 21.9M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 2% (200 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Nansen x HypurrCollective"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Nansen x HypurrCollective"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Nansen x HypurrCollective"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Nansen x HypurrCollective"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Nansen x HypurrCollective"}[24h])
+
+ - slug: infinitefield-xyz
+ name: "infinitefield.xyz"
+ tag: "0% commission · 18.2M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 0% — takes no cut from delegators."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="infinitefield.xyz"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="infinitefield.xyz"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="infinitefield.xyz"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="infinitefield.xyz"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="infinitefield.xyz"}[24h])
+
+ - slug: hyper-foundation-5
+ name: "Hyper Foundation 5"
+ tag: "3% commission · 15.3M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 3% (300 bps). Hyper Foundation validator."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyper Foundation 5"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Hyper Foundation 5"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Hyper Foundation 5"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Hyper Foundation 5"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyper Foundation 5"}[24h])
+
+ - slug: hyperstake
+ name: "HyperStake"
+ tag: "0% commission · 9.9M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 0% — takes no cut from delegators."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="HyperStake"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="HyperStake"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="HyperStake"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="HyperStake"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="HyperStake"}[24h])
+
+ - slug: cmi
+ name: "CMI"
+ tag: "0% commission · 9.3M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 0% — takes no cut from delegators."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="CMI"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="CMI"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="CMI"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="CMI"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="CMI"}[24h])
+
+ - slug: hyperdash
+ name: "Hyperdash"
+ tag: "3% commission · 9.2M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 3% (300 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyperdash"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Hyperdash"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Hyperdash"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Hyperdash"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyperdash"}[24h])
+
+ - slug: asxn
+ name: "ASXN"
+ tag: "4.9% commission · 8.8M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 4.9% (490 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="ASXN"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="ASXN"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="ASXN"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="ASXN"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="ASXN"}[24h])
+
+ - slug: hypurrcorea-spacebar-x-despread
+ name: "HypurrCorea - Spacebar x DeSpread"
+ tag: "3% commission · 8.2M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 3% (300 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="HypurrCorea - Spacebar x DeSpread"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="HypurrCorea - Spacebar x DeSpread"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="HypurrCorea - Spacebar x DeSpread"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="HypurrCorea - Spacebar x DeSpread"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="HypurrCorea - Spacebar x DeSpread"}[24h])
+
+ - slug: usdt0-x-luganodes
+ name: "USDT0 x Luganodes"
+ tag: "3% commission · 7.5M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 3% (300 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="USDT0 x Luganodes"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="USDT0 x Luganodes"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="USDT0 x Luganodes"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="USDT0 x Luganodes"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="USDT0 x Luganodes"}[24h])
+
+ - slug: purrposeful-x-hybridge-x-pip
+ name: "Purrposeful x HyBridge x PiP"
+ tag: "1.75% commission · 7.2M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 1.75% (175 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Purrposeful x HyBridge x PiP"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Purrposeful x HyBridge x PiP"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Purrposeful x HyBridge x PiP"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Purrposeful x HyBridge x PiP"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Purrposeful x HyBridge x PiP"}[24h])
+
+ - slug: kinetiq-x-hyperion
+ name: "Kinetiq x Hyperion"
+ tag: "4% commission · 7.0M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 4% (400 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Kinetiq x Hyperion"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Kinetiq x Hyperion"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Kinetiq x Hyperion"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Kinetiq x Hyperion"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Kinetiq x Hyperion"}[24h])
+
+ - slug: validao
+ name: "ValiDAO"
+ tag: "4% commission · 6.7M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 4% (400 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="ValiDAO"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="ValiDAO"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="ValiDAO"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="ValiDAO"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="ValiDAO"}[24h])
+
+ - slug: bitwise-onchain-solutions-x-falconx
+ name: "Bitwise Onchain Solutions x FalconX"
+ tag: "3% commission · 5.6M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 3% (300 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Bitwise Onchain Solutions x FalconX"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Bitwise Onchain Solutions x FalconX"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Bitwise Onchain Solutions x FalconX"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Bitwise Onchain Solutions x FalconX"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Bitwise Onchain Solutions x FalconX"}[24h])
+
+ - slug: alphaticks
+ name: "Alphaticks"
+ tag: "1% commission · 5.5M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 1% (100 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Alphaticks"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Alphaticks"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Alphaticks"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Alphaticks"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Alphaticks"}[24h])
+
+ - slug: b-harvest
+ name: "B-Harvest"
+ tag: "5% commission · 5.2M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 5% (500 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="B-Harvest"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="B-Harvest"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="B-Harvest"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="B-Harvest"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="B-Harvest"}[24h])
+
+ - slug: flowdex
+ name: "Flowdex"
+ tag: "0% commission · 5.1M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 0% — takes no cut from delegators."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Flowdex"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Flowdex"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Flowdex"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Flowdex"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Flowdex"}[24h])
+
+ - slug: liquid-spirit-x-hydromancer-x-rekt-gang
+ name: "Liquid Spirit x Hydromancer x Rekt Gang"
+ tag: "3% commission · 4.5M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 3% (300 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Liquid Spirit x Hydromancer x Rekt Gang"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Liquid Spirit x Hydromancer x Rekt Gang"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Liquid Spirit x Hydromancer x Rekt Gang"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Liquid Spirit x Hydromancer x Rekt Gang"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Liquid Spirit x Hydromancer x Rekt Gang"}[24h])
+
+ - slug: hyperbeat-x-p2p-x-hypio
+ name: "Hyperbeat x P2P x Hypio"
+ tag: "2% commission · 3.9M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 2% (200 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyperbeat x P2P x Hypio"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Hyperbeat x P2P x Hypio"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Hyperbeat x P2P x Hypio"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Hyperbeat x P2P x Hypio"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Hyperbeat x P2P x Hypio"}[24h])
+
+ - slug: enigma-hypedexer-com-x-meria
+ name: "Enigma - Hypedexer.com X Meria"
+ tag: "3% commission · 3.3M HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 3% (300 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Enigma - Hypedexer.com X Meria"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Enigma - Hypedexer.com X Meria"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Enigma - Hypedexer.com X Meria"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Enigma - Hypedexer.com X Meria"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Enigma - Hypedexer.com X Meria"}[24h])
+
+ - slug: galaxydigital
+ name: "GalaxyDigital"
+ tag: "5% commission · 640K HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 5% (500 bps)."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="GalaxyDigital"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="GalaxyDigital"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="GalaxyDigital"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="GalaxyDigital"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="GalaxyDigital"}[24h])
+
+ - slug: meria
+ name: "Meria"
+ tag: "2.9% commission · 1.6K HYPE staked"
+ formula: "Net yield = predictedApr.day × uptimeFraction. Commission 2.9% (290 bps). Near-zero stake."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Meria"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Meria"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Meria"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Meria"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Meria"}[24h])
+
+ - slug: figment
+ name: "Figment"
+ tag: "5% commission · JAILED"
+ formula: "Net yield = 0 (jailed). Commission 5% (500 bps). Jailed validators earn no rewards."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Figment"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Figment"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Figment"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Figment"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Figment"}[24h])
+
+ - slug: falcon-capital
+ name: "Falcon Capital"
+ tag: "100% commission · JAILED"
+ formula: "Net yield = 0 (jailed). Commission 100% (10000 bps). Jailed validators earn no rewards."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Falcon Capital"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Falcon Capital"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Falcon Capital"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Falcon Capital"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Falcon Capital"}[24h])
+
+ - slug: asymmetric-research
+ name: "Asymmetric Research"
+ tag: "1.5% commission · JAILED"
+ formula: "Net yield = 0 (jailed). Commission 1.5% (150 bps). Jailed validators earn no rewards."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Asymmetric Research"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Asymmetric Research"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Asymmetric Research"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Asymmetric Research"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Asymmetric Research"}[24h])
+
+ - slug: hypercn-x-hlscan
+ name: "HyperCN X hlscan"
+ tag: "1% commission · JAILED"
+ formula: "Net yield = 0 (jailed). Commission 1% (100 bps). Jailed validators earn no rewards."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="HyperCN X hlscan"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="HyperCN X hlscan"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="HyperCN X hlscan"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="HyperCN X hlscan"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="HyperCN X hlscan"}[24h])
+
+ - slug: red-pond
+ name: "Red Pond"
+ tag: "1% commission · JAILED"
+ formula: "Net yield = 0 (jailed). Commission 1% (100 bps). Jailed validators earn no rewards."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Red Pond"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="Red Pond"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="Red Pond"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="Red Pond"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="Red Pond"}[24h])
+
+ - slug: cp0x-by-stakr-space
+ name: "cp0x by STAKR.space"
+ tag: "0% commission · JAILED"
+ formula: "Net yield = 0 (jailed). Commission 0%. Jailed validators earn no rewards."
+ queries:
+ p50: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="cp0x by STAKR.space"}[24h])
+ success: clamp_max(avg_over_time(ocb_validator_uptime_pct{chain="hyperliquid",name="cp0x by STAKR.space"}[24h]) / 100, 1)
+ mean: ocb_validator_commission_bps{chain="hyperliquid",name="cp0x by STAKR.space"}
+ sample_size: ocb_validator_stake_usd{chain="hyperliquid",name="cp0x by STAKR.space"}
+ series: avg_over_time(ocb_validator_net_yield_bps{chain="hyperliquid",name="cp0x by STAKR.space"}[24h])
diff --git a/src/components/benchmark-body.tsx b/src/components/benchmark-body.tsx
index f12775ef..1db2ea88 100644
--- a/src/components/benchmark-body.tsx
+++ b/src/components/benchmark-body.tsx
@@ -586,12 +586,19 @@ export function BenchmarkBody({
}, [hasLongHistory, longRangeKey, hlArchiveCache]);
// Derive the chart's `longRangeSeries` prop from the archive cache.
- // Each entry maps `slug -> daily-fees array` because the HL bench
- // headline metric is builder fees collected (USD). The leaderboard
- // below independently shows volume + fees + fills, so the wire shape
- // carries all three — only `fees` is fed to the chart Y-axis.
+ // The mapped field is chosen based on the active panel so that switching
+ // to a users/volume/fees panel also updates the long-range chart Y-axis.
const hlLongRangeSeries = useMemo(() => {
if (!hasLongHistory) return undefined;
+ // Pick the archive field that matches the active panel metric so the
+ // chart shows the right series when a panel (users / volume / fees) is
+ // selected and the Prom 90d/1y panel series isn't available.
+ const field =
+ activePanelId === "users" || activePanelId === "users_7d" || activePanelId === "users_30d"
+ ? "users" as const
+ : activePanelId === "volume" || activePanelId === "volume_7d" || activePanelId === "volume_30d"
+ ? "vol" as const
+ : "fees" as const;
const map: Partial>> = {};
for (const w of LONG_RANGES) {
const cached = hlArchiveCache[w];
@@ -599,15 +606,14 @@ export function BenchmarkBody({
const ts = cached.timeseries_daily ?? {};
const perBuilder: Record = {};
for (const [slug, days] of Object.entries(ts)) {
- // Builder fees collected. If the metric mapping later grows to
- // include companion charts (volume / fills), this branch picks
- // the matching field per metric label.
- perBuilder[slug] = days.map((d) => d.fees);
+ perBuilder[slug] = days.map((d) =>
+ field === "users" ? (d.users ?? 0) : field === "vol" ? d.vol : d.fees
+ );
}
map[w] = perBuilder;
}
return map;
- }, [hasLongHistory, hlArchiveCache]);
+ }, [hasLongHistory, hlArchiveCache, activePanelId]);
const hlActiveArchive: HlArchiveHistoryResponse | null = useMemo(() => {
if (!hasLongHistory || !longRangeKey) return null;
diff --git a/src/components/time-series-chart/index.tsx b/src/components/time-series-chart/index.tsx
index 8fd3ea42..3839ef4e 100644
--- a/src/components/time-series-chart/index.tsx
+++ b/src/components/time-series-chart/index.tsx
@@ -380,12 +380,19 @@ export function TimeSeriesChart({
if (range === "7d") {
return seriesOverride7d ?? panelLazy7d ?? seriesOverride;
}
- // 90d / 1y: served from Prom via lazy fetch. Return empty map while
- // loading so the chart shows an empty state rather than stale 24h data.
- // 180d / all: no panel data — these tabs stay disabled for panels.
- if (range === "90d") return panelLazy90d ?? {};
- if (range === "1y") return panelLazy1y ?? {};
- return seriesOverride;
+ // 90d / 1y: use Prom-backed lazy series when available, otherwise
+ // return undefined so pickBenchValues falls through to longRangeSeries
+ // (archive). An empty map would hide the archive lines.
+ // 180d / all: no Prom panel data; fall through to longRangeSeries.
+ if (range === "90d") {
+ if (panelLazy90d && Object.keys(panelLazy90d).length > 0) return panelLazy90d;
+ return undefined;
+ }
+ if (range === "1y") {
+ if (panelLazy1y && Object.keys(panelLazy1y).length > 0) return panelLazy1y;
+ return undefined;
+ }
+ return undefined;
};
const panel = pickPanel();
const sliceOverride = (full: (number | null)[]): (number | null)[] => {
@@ -542,17 +549,16 @@ export function TimeSeriesChart({
{/* Hairline separator between live (Prom) and archive
ranges so the reader sees they come from a different
data source — kept inside the same pill strip so the
- selection feels like one control. Panel tabs disable
- the archive ranges (panel data is not archived). */}
+ selection feels like one control. */}
{LONG_RANGES.map((r) => {
const active = r === range;
- // 90d / 1y are Prom-backed for panel views; 180d / all are
- // archive-only and stay disabled when a panel is active.
- const panelBlocks = panelActive && r !== "90d" && r !== "1y";
+ // Archive series covers all long-range windows (90d/180d/1y/all)
+ // even when a panel is active — panelBlocks no longer needed.
+ const panelBlocks = false;
const disabled = longRangeDisabled || panelBlocks;
const title = panelBlocks
? "Switch to the main metric for 180D / ALL history"
From 765202404a289cbb844ed9d201b054230249f7bf Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 15:48:30 +0200
Subject: [PATCH 05/14] feat(pm-cohort-stats): add polymarket-us active market
counter
---
harnesses/pm-cohort-stats/cmd/script/main.go | 21 ++++
.../cmd/script/polymarket_us.go | 116 ++++++++++++++++++
.../pm-cohort-stats/cmd/script/registry.go | 11 +-
3 files changed, 143 insertions(+), 5 deletions(-)
create mode 100644 harnesses/pm-cohort-stats/cmd/script/polymarket_us.go
diff --git a/harnesses/pm-cohort-stats/cmd/script/main.go b/harnesses/pm-cohort-stats/cmd/script/main.go
index 75bd332e..966d6b35 100644
--- a/harnesses/pm-cohort-stats/cmd/script/main.go
+++ b/harnesses/pm-cohort-stats/cmd/script/main.go
@@ -92,6 +92,12 @@ func main() {
runManifoldLoop(cfg, stop)
}()
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ runPolymarketUSLoop(cfg, stop)
+ }()
+
<-sigChan
fmt.Println("\nShutting down...")
close(stop)
@@ -187,3 +193,18 @@ func runManifoldLoop(cfg *Config, stop <-chan struct{}) {
}
}
}
+
+func runPolymarketUSLoop(cfg *Config, stop <-chan struct{}) {
+ tick := time.NewTicker(cfg.PolymarketRefreshInterval)
+ defer tick.Stop()
+
+ fetchAllPolymarketUS()
+ for {
+ select {
+ case <-stop:
+ return
+ case <-tick.C:
+ fetchAllPolymarketUS()
+ }
+ }
+}
diff --git a/harnesses/pm-cohort-stats/cmd/script/polymarket_us.go b/harnesses/pm-cohort-stats/cmd/script/polymarket_us.go
new file mode 100644
index 00000000..8188e8e1
--- /dev/null
+++ b/harnesses/pm-cohort-stats/cmd/script/polymarket_us.go
@@ -0,0 +1,116 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+// Polymarket US (QCX) is the CFTC-regulated DCM operated by QCX Inc,
+// accessible at https://polymarketexchange.com. It mirrors the Polymarket
+// Global market catalog under CFTC supervision and offers retail access
+// via Robinhood.
+//
+// Public API: https://gateway.polymarket.us/v1
+// GET /markets?limit=100&offset=N&active=true&closed=false
+// Returns up to 100 market rows, no volume/OI fields.
+//
+// Volume and OI are NOT available in the public REST catalog:
+// - The /markets list only carries bid/ask quotes and market metadata.
+// - The /markets/{slug}/bbo endpoint gives per-market OI but would
+// require one request per market — prohibitive for a 30k+ catalog.
+// - There is no /stats aggregate endpoint.
+//
+// We therefore publish:
+// pm_venue_active_markets{venue="polymarket-us"} counted from pagination
+// (all other pm_venue_* gauges stay at their zero default = null in Prom)
+//
+// Rate budget: 50 pages * 100 markets = 5000 markets counted per tick.
+// At 5-min tick intervals and 20 req/s gateway cap: ~0.1 req/s average,
+// well within budget.
+
+const (
+ polymarketUSBase = "https://gateway.polymarket.us"
+ polymarketUSMaxPages = 50
+ polymarketUSLimit = 100
+ polymarketUSUA = "OCB-pm-cohort-stats/1.0 (+https://openchainbench.com/methodology; contact@mobula.io)"
+)
+
+var httpClientPolymarketUS = &http.Client{Timeout: 15 * time.Second}
+
+type polymarketUSMarket struct {
+ Active bool `json:"active"`
+ Closed bool `json:"closed"`
+ Slug string `json:"slug"`
+}
+
+type polymarketUSListResponse struct {
+ Markets []polymarketUSMarket `json:"markets"`
+}
+
+// fetchAllPolymarketUS counts active markets on Polymarket US and writes
+// pm_venue_active_markets{venue="polymarket-us"}. Volume and OI remain at
+// their zero default because the public gateway does not expose aggregate
+// financial metrics.
+func fetchAllPolymarketUS() {
+ start := time.Now()
+ const slug = "polymarket-us"
+ const source = "gateway-polymarket-us"
+
+ activeCount, err := countPolymarketUSActiveMarkets()
+ if err != nil {
+ pmCohortStatsFetchErrors.WithLabelValues(slug, source, classifyError(err.Error())).Inc()
+ fmt.Printf("[polymarket-us] count error: %v\n", err)
+ return
+ }
+
+ pmVenueActiveMarkets.WithLabelValues(slug).Set(float64(activeCount))
+ pmCohortStatsLastRefresh.WithLabelValues(slug, source).SetToCurrentTime()
+ pmCohortStatsFetchLatencyMs.WithLabelValues(slug, source).Set(float64(time.Since(start).Milliseconds()))
+ pmCohortStatsLastTickUnix.SetToCurrentTime()
+ fmt.Printf("[polymarket-us] active_markets=%d latency=%dms\n", activeCount, time.Since(start).Milliseconds())
+}
+
+func countPolymarketUSActiveMarkets() (int, error) {
+ total := 0
+ for page := 0; page < polymarketUSMaxPages; page++ {
+ offset := page * polymarketUSLimit
+ u, _ := url.Parse(polymarketUSBase + "/v1/markets")
+ q := u.Query()
+ q.Set("limit", fmt.Sprintf("%d", polymarketUSLimit))
+ q.Set("offset", fmt.Sprintf("%d", offset))
+ q.Set("active", "true")
+ q.Set("closed", "false")
+ u.RawQuery = q.Encode()
+
+ req, err := http.NewRequest("GET", u.String(), nil)
+ if err != nil {
+ return total, fmt.Errorf("build request: %w", err)
+ }
+ req.Header.Set("User-Agent", polymarketUSUA)
+
+ resp, err := httpClientPolymarketUS.Do(req)
+ if err != nil {
+ return total, fmt.Errorf("fetch page %d: %w", page, err)
+ }
+
+ var body polymarketUSListResponse
+ if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
+ resp.Body.Close()
+ return total, fmt.Errorf("decode page %d: %w", page, err)
+ }
+ resp.Body.Close()
+
+ n := len(body.Markets)
+ total += n
+ if n < polymarketUSLimit {
+ break
+ }
+
+ // Courtesy inter-page delay: stay well under the 20 req/s gateway cap.
+ time.Sleep(150 * time.Millisecond)
+ }
+ return total, nil
+}
diff --git a/harnesses/pm-cohort-stats/cmd/script/registry.go b/harnesses/pm-cohort-stats/cmd/script/registry.go
index fbab9f38..0cb3db25 100644
--- a/harnesses/pm-cohort-stats/cmd/script/registry.go
+++ b/harnesses/pm-cohort-stats/cmd/script/registry.go
@@ -26,11 +26,12 @@ type Venue struct {
// Order = display order in the PM hub.
// Adding a new venue: append here, append on the OCB site, redeploy both.
var Registry = []Venue{
- {Slug: "polymarket", Name: "Polymarket", Type: "onchain", Chain: "polygon"},
- {Slug: "kalshi", Name: "Kalshi", Type: "offchain", Chain: ""},
- {Slug: "limitless", Name: "Limitless", Type: "onchain", Chain: "base"},
- {Slug: "manifold", Name: "Manifold", Type: "offchain", Chain: ""},
- {Slug: "myriad", Name: "Myriad", Type: "onchain", Chain: "abstract"},
+ {Slug: "polymarket", Name: "Polymarket", Type: "onchain", Chain: "polygon"},
+ {Slug: "polymarket-us", Name: "Polymarket US", Type: "offchain", Chain: ""},
+ {Slug: "kalshi", Name: "Kalshi", Type: "offchain", Chain: ""},
+ {Slug: "limitless", Name: "Limitless", Type: "onchain", Chain: "base"},
+ {Slug: "manifold", Name: "Manifold", Type: "offchain", Chain: ""},
+ {Slug: "myriad", Name: "Myriad", Type: "onchain", Chain: "abstract"},
}
// VenueBySlug returns the Venue with the given slug, or nil if not found.
From f9a5b2c0f3123bc1dd2eb6e60f8ea2a6753b126e Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 15:48:41 +0200
Subject: [PATCH 06/14] fix: drop DefiLlama from dex-network-coverage, fix hub
value formatting
---
benchmarks/dex-network-coverage.yml | 48 +++++----------
.../network-coverage/cmd/script/defillama.go | 60 -------------------
harnesses/network-coverage/cmd/script/main.go | 1 -
src/lib/data-api-stats.ts | 20 +++++--
4 files changed, 29 insertions(+), 100 deletions(-)
delete mode 100644 harnesses/network-coverage/cmd/script/defillama.go
diff --git a/benchmarks/dex-network-coverage.yml b/benchmarks/dex-network-coverage.yml
index 29b361bd..19852251 100644
--- a/benchmarks/dex-network-coverage.yml
+++ b/benchmarks/dex-network-coverage.yml
@@ -4,7 +4,7 @@ slug: dex-network-coverage
number: "090"
title: DEX indexer with most blockchains, live coverage ranking
seo_title: "DEX indexer most chains 2026"
-seo_description: "Which DEX indexer supports the most blockchains? Live coverage ranking across GeckoTerminal, Codex, DefiLlama, DexPaprika and Sim by Dune."
+seo_description: "Which DEX indexer supports the most blockchains? Live coverage ranking across GeckoTerminal, Codex, DexPaprika and Sim by Dune."
subtitle: Number of blockchains where each major DEX indexer actively indexes pools + swap volumes, audited every six hours against each provider's public networks endpoint.
category: Aggregators
status: live
@@ -20,29 +20,28 @@ seo_intro: |
can this API return pool addresses, live swap events, OHLCV candles
and trending pools. Providers on this leaderboard run their own DEX
pool indexer as a product (GeckoTerminal `/networks`, Codex
- `getNetworks`, DefiLlama `/chains`, DexPaprika `/networks`, Sim by
- Dune `/evm/supported-chains`). Asset-registry coverage (chains where
- an API knows tokens for contract-address lookup) is a different
- question measured on the sister bench asset-registry-coverage.
+ `getNetworks`, DexPaprika `/networks`, Sim by Dune
+ `/evm/supported-chains`). Asset-registry coverage (chains where an
+ API knows tokens for contract-address lookup) is a different question
+ measured on the sister bench asset-registry-coverage.
abstract: |
We benchmark how many networks each major DEX indexer publishes as
actively indexed for pools + swap volumes. The harness fetches the
- official listing every six hours from five providers (GeckoTerminal,
- Codex, DefiLlama, DexPaprika, Sim by Dune), deduplicates by chain id
- or platform slug and counts. Mainnet only. Asset-registry coverage
- (chains where the provider knows tokens for contract-address lookups)
- is measured on bench asset-registry-coverage because it answers a
- different question, an API can list tokens on 300 chains through a
- market-data pipeline while running DEX pool indexing on only a
- dozen. Both leaderboards live under Aggregators so a reader can
- compare a provider on the axis that matches their product.
+ official listing every six hours from four providers (GeckoTerminal,
+ Codex, DexPaprika, Sim by Dune), deduplicates by chain id or platform
+ slug and counts. Mainnet only. Asset-registry coverage (chains where
+ the provider knows tokens for contract-address lookups) is measured
+ on bench asset-registry-coverage because it answers a different
+ question, an API can list tokens on 300 chains through a market-data
+ pipeline while running DEX pool indexing on only a dozen. Both
+ leaderboards live under Aggregators so a reader can compare a
+ provider on the axis that matches their product.
methodology:
- "Source: each provider's public DEX-indexer networks endpoint."
- "GeckoTerminal: GET /api/v2/networks (paginated, no auth). Chains where GeckoTerminal actively indexes DEX pools + trades."
- "Codex: GraphQL `getNetworks` query at https://graph.codex.io/graphql with an official API key. Chains covered by Defined.fi's DEX data pipeline."
- - "DefiLlama: GET https://api.llama.fi/chains (no auth). Chains tracked by DefiLlama's TVL crawler, every chain with at least one indexed DeFi protocol, DEX chains are a subset."
- "DexPaprika: GET https://api.dexpaprika.com/networks (no auth). CoinPaprika's dedicated DEX tracker product, separate from the market-data API."
- "Sim by Dune: GET https://api.sim.dune.com/v1/evm/supported-chains (no auth). EVM only, mainnets filtered via the `mainnet` tag."
- "Cadence: full refresh every 6 hours."
@@ -53,7 +52,6 @@ findings:
- "{{best_name}} currently leads at {{best_p50}} chains across {{count}} measured providers. The count is read live from each provider's DEX-networks endpoint, so the number reflects where each API actively indexes DEX pools + swap volumes today, not a marketing claim."
- "{{name:geckoterminal}} sits at {{p50:geckoterminal}} chains via `/api/v2/networks`. GeckoTerminal indexes wherever CoinGecko's DEX pipeline reaches, so the count tracks the CoinGecko DEX-rollout roadmap rather than a standalone listing decision."
- "{{name:codex}} clocks {{p50:codex}} chains via `getNetworks`. Defined.fi historically prioritises depth on EVM + Solana DEX coverage rather than breadth across niche chains, which surfaces here as a tighter count with stronger per-chain DEX quality than a raw breadth ranking implies."
- - "{{name:defillama}} publishes {{p50:defillama}} chains via `/chains`. Broader than DEX-only (includes lending, LSTs, restaking, bridges) because the endpoint tracks every chain with at least one indexed DeFi protocol, DEX chains are a subset. The count is the canonical open reference for chains with meaningful onchain financial activity."
- "DEX-indexer breadth is one axis of a data API. Asset-registry breadth (bench asset-registry-coverage) and per-chain head lag (bench aggregator-head-lag) measure the other axes a serious DEX-integration decision needs."
source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/network-coverage
@@ -67,8 +65,6 @@ faq:
a: "{{best_name}} currently leads at {{best_p50}} chains across {{count}} measured providers. The count is read every six hours directly from each provider's own DEX-networks endpoint and deduplicated by chain id, so the leaderboard reflects where each API actively indexes DEX pools + swap volumes today, not a claim from a marketing page."
- q: "Why is this bench separate from asset-registry-coverage?"
a: "DEX-pool coverage and asset-registry coverage are two different products, not a single metric. GeckoTerminal indexes DEX pools on ~265 chains via `/networks`; CoinPaprika lists 300+ chains in `/v1/contracts` because it indexes token metadata everywhere. Comparing them on one leaderboard makes the number meaningless, a market-data API always wins on chain count while a DEX indexer always wins on pool-level depth. This bench answers the DEX-indexing question. The sister bench asset-registry-coverage answers the token-lookup question."
- - q: "Does DefiLlama really index DEX pools on 400+ chains?"
- a: "DefiLlama's `/chains` endpoint tracks every chain with at least one indexed DeFi protocol (DEXes, lending, LSTs, restaking, bridges, oracles), so the count is broader than DEX-only. DEX chains are a subset. DefiLlama sits on the leaderboard because the same crawler backs their DEX-volumes aggregator, and the endpoint is the canonical open reference for `chains with meaningful onchain financial activity`. Read alongside the more DEX-specific counts from GeckoTerminal, Codex and DexPaprika for the DEX-only view."
- q: "Why isn't CoinGecko / Mobula / CoinPaprika on this leaderboard?"
a: "Those providers are measured on the sister bench asset-registry-coverage because their product is chain-scoped token metadata + market data, not DEX pool indexing. CoinGecko powers GeckoTerminal on the DEX side, but the token-registry number and the DEX-indexed number are measured separately because they answer different questions. Mobula and CoinPaprika are market-data APIs, not DEX pool indexers, DexPaprika is CoinPaprika's dedicated DEX product and is on this leaderboard."
- q: "How often is the chain count refreshed?"
@@ -77,8 +73,7 @@ faq:
a: "Any chain a provider lists as a production network for DEX indexing, identified by a unique chain id or slug. Ethereum, Solana, Base, Arbitrum, BNB, Avalanche and so on each count once regardless of how the provider labels them internally. Testnet listings are filtered server-side where the provider exposes a testnet flag. Sim by Dune is EVM only by construction of the `/v1/evm/supported-chains` endpoint."
# Real metrics exposed by the network-coverage harness:
-# networks_supported_total{provider="geckoterminal"|"codex"|
-# "defillama"|"dexpaprika"|"dune"}
+# networks_supported_total{provider="geckoterminal"|"codex"|"dexpaprika"|"dune"}
# -> gauge, the unique-chain count from the latest successful refresh.
providers:
@@ -108,19 +103,6 @@ providers:
sample_size: networks_supported_total{provider="codex"}
series: networks_supported_total{provider="codex"}
- - slug: defillama
- name: DefiLlama
- tag: TVL + DEX-volumes aggregator
- formula: "Count of chains returned by DefiLlama's public `/chains` endpoint (chains with at least one indexed DeFi protocol; DEX chains are a subset), refreshed every 6 hours."
- queries:
- p50: networks_supported_total{provider="defillama"}
- p90: networks_supported_total{provider="defillama"}
- p99: networks_supported_total{provider="defillama"}
- mean: networks_supported_total{provider="defillama"}
- success: clamp_max(networks_supported_total{provider="defillama"} > bool 0, 1)
- sample_size: networks_supported_total{provider="defillama"}
- series: networks_supported_total{provider="defillama"}
-
- slug: dexpaprika
name: DexPaprika
tag: CoinPaprika's DEX product
diff --git a/harnesses/network-coverage/cmd/script/defillama.go b/harnesses/network-coverage/cmd/script/defillama.go
deleted file mode 100644
index 4a28310a..00000000
--- a/harnesses/network-coverage/cmd/script/defillama.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "time"
-)
-
-// DefiLlama tracks TVL across every chain with at least one indexed
-// protocol. Broader than "DEX-only" (includes lending, LSTs, restaking,
-// bridges, oracles), but it is the canonical open reference for
-// "chains with meaningful on-chain financial activity" — and the same
-// crawler backs their DEX-volumes aggregator, so DEX chains are a
-// subset of what /chains returns.
-const defiLlamaChainsURL = "https://api.llama.fi/chains"
-
-type defiLlamaChain struct {
- Name string `json:"name"`
- ChainID any `json:"chainId"` // number for EVM chains, null elsewhere
- Gecko string `json:"gecko_id"`
- TVL float64 `json:"tvl"`
- CMCID string `json:"cmcId"`
- TokenSym string `json:"tokenSymbol"`
-}
-
-func fetchDefiLlama(_ *Config) ProviderResult {
- res := ProviderResult{Provider: "defillama"}
- client := &http.Client{Timeout: 15 * time.Second}
- req, _ := http.NewRequest("GET", defiLlamaChainsURL, nil)
- req.Header.Set("Accept", "application/json")
- req.Header.Set("User-Agent", "OpenChainBench/1.0 (+https://openchainbench.com)")
-
- resp, err := client.Do(req)
- if err != nil {
- res.Err = fmt.Sprintf("request_error: %v", err)
- return res
- }
- defer resp.Body.Close()
- body, _ := io.ReadAll(resp.Body)
- if resp.StatusCode != 200 {
- res.Err = fmt.Sprintf("status_%d", resp.StatusCode)
- return res
- }
-
- var parsed []defiLlamaChain
- if err := json.Unmarshal(body, &parsed); err != nil {
- res.Err = fmt.Sprintf("parse_error: %v", err)
- return res
- }
- for _, c := range parsed {
- res.Networks = append(res.Networks, Network{
- ChainID: fmt.Sprintf("%v", c.ChainID),
- Slug: c.Gecko,
- Name: c.Name,
- })
- }
- return res
-}
diff --git a/harnesses/network-coverage/cmd/script/main.go b/harnesses/network-coverage/cmd/script/main.go
index 4133fe1f..885a9ffa 100644
--- a/harnesses/network-coverage/cmd/script/main.go
+++ b/harnesses/network-coverage/cmd/script/main.go
@@ -75,7 +75,6 @@ func fetchAll(cfg *Config) {
{"dune", fetchSimDune},
{"coinstats", fetchCoinStats},
{"coingecko", fetchCoinGecko},
- {"defillama", fetchDefiLlama},
{"dexpaprika", fetchDexPaprika},
}
diff --git a/src/lib/data-api-stats.ts b/src/lib/data-api-stats.ts
index f8a0a084..d563c52a 100644
--- a/src/lib/data-api-stats.ts
+++ b/src/lib/data-api-stats.ts
@@ -282,13 +282,21 @@ export function fmtDataValue(v: number, unit: Benchmark["unit"]): string {
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`;
+ return v >= 1000 ? `${Math.round(v / 1000)}k` : String(Math.round(v));
+ case "s": {
+ // Convention: benches with unit "s" store ms in ms.p50 (see format.ts)
+ const s = v / 1000;
+ if (s >= 60) return `${(s / 60).toFixed(1)} min`;
+ if (s < 1) return `${Math.round(v)} ms`;
+ return `${s.toFixed(2)} s`;
+ }
+ case "sec": {
+ // True seconds (indexing-freshness, etc.)
+ if (v >= 60) return `${(v / 60).toFixed(1)} min`;
+ return `${v.toFixed(1)} s`;
+ }
case "ms":
+ if (v >= 1000) return `${(v / 1000).toFixed(2)} s`;
return `${Math.round(v)} ms`;
case "bps":
case "bp":
From 83aa937a515ed0c70a5065f905af93d6cdfe94d2 Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 15:58:02 +0200
Subject: [PATCH 07/14] fix: resolve CSS vars in share-card chip fallback, cap
snapshot to 16 series
---
src/app/benchmarks/[slug]/share-card/route.tsx | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/src/app/benchmarks/[slug]/share-card/route.tsx b/src/app/benchmarks/[slug]/share-card/route.tsx
index e84913cc..07abf13f 100644
--- a/src/app/benchmarks/[slug]/share-card/route.tsx
+++ b/src/app/benchmarks/[slug]/share-card/route.tsx
@@ -209,6 +209,14 @@ function CardProviderLogo({
);
}
+ // chipBackground / chipTextColor return CSS variables ("var(--color-ink-soft)",
+ // "var(--color-paper)", "var(--color-ink)") for unbranded or dark-brand providers.
+ // Satori cannot resolve CSS variables and crashes the whole ImageResponse stream.
+ // Resolve to the local hex palette variables instead.
+ const rawBg = chipBackground(slug);
+ const chipBg = rawBg.startsWith("#") ? rawBg : INK_SOFT;
+ const rawFg = chipTextColor(slug);
+ const chipFg = rawFg === "var(--color-ink)" ? INK : PAPER;
return (
,
chainLabel?: string | null
) {
+ // Cap series to avoid unreadable charts and Satori element-count limits
+ // on large benches (e.g. hyperliquid-frontends has 104 providers).
+ const MAX_SNAPSHOT_SERIES = 16;
const sorted = sortByP50(benchmark);
const seriesList = sorted
.map((r) => ({
@@ -1072,7 +1083,8 @@ async function renderSnapshot(
color: colors.get(r.slug) ?? INK_SOFT,
p50: r.ms.p50,
}))
- .filter((s) => s.values.length > 1);
+ .filter((s) => s.values.length > 1)
+ .slice(0, MAX_SNAPSHOT_SERIES);
const chartW = 1086;
const chartH = 280;
From 34480e791b7b5327cf5db1c3da8d9a0c93032a76 Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 15:58:58 +0200
Subject: [PATCH 08/14] feat(data-api): regional sub-scores in bench + provider
pivot
---
src/components/data-api-bench-groups.tsx | 2 +-
src/components/data-api-providers-pivot.tsx | 50 +++++++++++++-
src/lib/data-api-stats.ts | 73 +++++++++++++++++++++
3 files changed, 121 insertions(+), 4 deletions(-)
diff --git a/src/components/data-api-bench-groups.tsx b/src/components/data-api-bench-groups.tsx
index e57ae98b..da8e17ae 100644
--- a/src/components/data-api-bench-groups.tsx
+++ b/src/components/data-api-bench-groups.tsx
@@ -36,7 +36,7 @@ export function DataApiBenchGroups({ groups }: { groups: DataApiGroupRow[] }) {
Providers
Leader
Runners-up
-
By region / chain
+
By region / chain
diff --git a/src/components/data-api-providers-pivot.tsx b/src/components/data-api-providers-pivot.tsx
index 37e391ea..ad75450c 100644
--- a/src/components/data-api-providers-pivot.tsx
+++ b/src/components/data-api-providers-pivot.tsx
@@ -8,6 +8,7 @@ import {
GROUP_META,
fmtDataValue,
type DataApiProviderPivotRow,
+ type DataApiRegionScore,
type DataApiGroup,
} from "@/lib/data-api-stats";
@@ -215,8 +216,8 @@ function GroupCell({
const { bestRank, bestCell } = cell;
const accent = GROUP_META[group].accent;
+ const hasRegions = bestCell.regions && bestCell.regions.length > 0;
- // Color intensity by rank
const bg =
bestRank === 1
? `${accent}22`
@@ -233,10 +234,10 @@ function GroupCell({
return (
+ {/* Global rank + value */}
{bestCell.benchShortTitle.replace("coverage", "cov.").replace("freshness", "fresh.")}
+ {/* Regional sub-scores */}
+ {hasRegions && (
+
+ )}
+
+ );
+}
+
+function RegionSubScores({
+ regions,
+ unit,
+ accent,
+}: {
+ regions: DataApiRegionScore[];
+ unit: DataApiProviderPivotRow["cells"][number]["unit"];
+ accent: string;
+}) {
+ return (
+
+ {regions.map((r) => (
+
+
+ {r.label}
+
+
+ #{r.rank}
+
+
+ ))}
);
}
diff --git a/src/lib/data-api-stats.ts b/src/lib/data-api-stats.ts
index d563c52a..9f0d7b97 100644
--- a/src/lib/data-api-stats.ts
+++ b/src/lib/data-api-stats.ts
@@ -143,6 +143,13 @@ export type DataApiGroupRow = {
benches: DataApiBenchRow[];
};
+export type DataApiRegionScore = {
+ region: string;
+ label: string;
+ rank: number;
+ p50: number;
+};
+
export type DataApiProviderCell = {
benchSlug: string;
benchShortTitle: string;
@@ -151,6 +158,8 @@ export type DataApiProviderCell = {
p50: number;
unit: Benchmark["unit"];
higherIsBetter: boolean;
+ /** Per-region ranks for benches that have extras.regions populated. */
+ regions?: DataApiRegionScore[];
};
export type DataApiProviderPivotRow = {
@@ -251,6 +260,67 @@ function computeChainLeaders(bench: Benchmark): ChainLeader[] {
.sort((a, b) => a.label.localeCompare(b.label));
}
+const REGION_ORDER = ["us-east", "eu-west", "sgp"] as const;
+const REGION_LABELS: Record
= {
+ "us-east": "US",
+ "eu-west": "EU",
+ sgp: "SGP",
+ "ap-southeast": "SGP",
+};
+
+/**
+ * For each live provider in a bench, compute their rank per probe region.
+ * Returns a map: providerSlug → sorted DataApiRegionScore[].
+ * Empty map when bench has no extras.regions data.
+ */
+function computeProviderRegionScores(
+ bench: Benchmark,
+ live: ProviderResult[],
+): Map {
+ const { regions } = bench.extras;
+ if (!regions || Object.keys(regions).length === 0) return new Map();
+
+ // Collect per-region p50 for all live providers
+ const byRegion = new Map();
+ for (const provider of live) {
+ const pts = regions[provider.slug] ?? [];
+ for (const pt of pts) {
+ if (pt.region === "global") continue;
+ const norm = pt.region === "ap-southeast" ? "sgp" : pt.region;
+ if (!byRegion.has(norm)) byRegion.set(norm, []);
+ byRegion.get(norm)!.push({ slug: provider.slug, p50: pt.p50 });
+ }
+ }
+
+ // Sort each region and assign ranks
+ const result = new Map();
+ for (const [region, entries] of byRegion.entries()) {
+ const sorted = [...entries].sort((a, b) =>
+ bench.higherIsBetter ? b.p50 - a.p50 : a.p50 - b.p50,
+ );
+ for (const [idx, { slug, p50 }] of sorted.entries()) {
+ if (!result.has(slug)) result.set(slug, []);
+ result.get(slug)!.push({
+ region,
+ label: REGION_LABELS[region] ?? region.toUpperCase(),
+ rank: idx + 1,
+ p50,
+ });
+ }
+ }
+
+ // Sort each provider's scores in canonical region order
+ for (const scores of result.values()) {
+ scores.sort(
+ (a, b) =>
+ (REGION_ORDER as readonly string[]).indexOf(a.region) -
+ (REGION_ORDER as readonly string[]).indexOf(b.region),
+ );
+ }
+
+ return result;
+}
+
function toBenchRow(slug: string, bench: Benchmark): DataApiBenchRow {
const live = sortedResults(liveResults(bench), bench.higherIsBetter);
return {
@@ -344,10 +414,12 @@ async function buildSnapshot(): Promise {
const group = BENCH_GROUP[slug];
if (!group) continue;
const live = sortedResults(liveResults(bench), bench.higherIsBetter);
+ const regionScores = computeProviderRegionScores(bench, live);
for (const [idx, r] of live.entries()) {
if (!providerMap.has(r.slug)) {
providerMap.set(r.slug, { name: r.name, cells: [] });
}
+ const regions = regionScores.get(r.slug);
providerMap.get(r.slug)!.cells.push({
benchSlug: slug,
benchShortTitle: BENCH_SHORT_TITLE[slug] ?? slug,
@@ -356,6 +428,7 @@ async function buildSnapshot(): Promise {
p50: r.ms.p50,
unit: bench.unit,
higherIsBetter: bench.higherIsBetter,
+ regions: regions && regions.length > 0 ? regions : undefined,
});
}
}
From fa32417e23a001845eb9c70c10d50496b5dfbc6f Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 16:13:20 +0200
Subject: [PATCH 09/14] fix(data-api): cap chain strip to 3 pills + overflow
count
---
src/components/data-api-bench-groups.tsx | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/components/data-api-bench-groups.tsx b/src/components/data-api-bench-groups.tsx
index da8e17ae..f2d07730 100644
--- a/src/components/data-api-bench-groups.tsx
+++ b/src/components/data-api-bench-groups.tsx
@@ -223,9 +223,13 @@ function ChainStrip({
leaders: ChainLeader[];
unit: DataApiBenchRow["unit"];
}) {
+ const MAX = 3;
+ const visible = leaders.slice(0, MAX);
+ const overflow = leaders.length - MAX;
+
return (
-
- {leaders.map((l) => (
+
+ {visible.map((l) => (
))}
+ {overflow > 0 && (
+
+{overflow}
+ )}
);
}
From b2bbaa09c37392dbe261ce32ee6d0bf072ae85bb Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 16:25:22 +0200
Subject: [PATCH 10/14] fix(export): capture only chart body to remove
header/pills whitespace
---
src/components/time-series-chart/index.tsx | 45 ++++++++++++----------
1 file changed, 25 insertions(+), 20 deletions(-)
diff --git a/src/components/time-series-chart/index.tsx b/src/components/time-series-chart/index.tsx
index 3839ef4e..c2e7fcb8 100644
--- a/src/components/time-series-chart/index.tsx
+++ b/src/components/time-series-chart/index.tsx
@@ -135,9 +135,12 @@ export function TimeSeriesChart({
// Chart's internal re-renders. Reset to null when range or region
// changes (the data shape is different, the old zoom doesn't apply).
const [zoom, setZoom] = useState<{ startFrac: number; endFrac: number } | null>(null);
- // Ref to the
so ChartExportButton can rasterise the whole
- // chart (header + SVG + watermark + legend) in one shot.
const figureRef = useRef(null);
+ // Points to just the SVG+legend block so the exported PNG captures
+ // only the chart body — the omitted header/pills divs still occupy
+ // layout space inside and would create blank whitespace at
+ // the top of the image if we used figureRef as the export target.
+ const chartBodyRef = useRef(null);
const zoomScopeKey = `${range}|${region}`;
const [prevZoomScopeKey, setPrevZoomScopeKey] = useState(zoomScopeKey);
if (prevZoomScopeKey !== zoomScopeKey) {
@@ -494,7 +497,7 @@ export function TimeSeriesChart({
{zoom && (
@@ -619,23 +622,25 @@ export function TimeSeriesChart({
- {lines.length === 0 ? (
-
- No time-series data emitted for this range yet.
-
- ) : (
- 0 ? reset : undefined}
- />
- )}
+
+ {lines.length === 0 ? (
+
+ No time-series data emitted for this range yet.
+
+ ) : (
+
0 ? reset : undefined}
+ />
+ )}
+
);
}
From d1f976787324cce2db70a4981a6f0ecae03ac370 Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 16:35:32 +0200
Subject: [PATCH 11/14] fix(pm): add polymarket-us logo (reuse polymarket.png)
---
src/lib/logo-manifest.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts
index 42781bf3..5cdaf6a1 100644
--- a/src/lib/logo-manifest.ts
+++ b/src/lib/logo-manifest.ts
@@ -61,6 +61,7 @@ const RAW: Record = {
mobula: "/logos/mobula.svg",
codex: "/logos/codex.svg",
polymarket: "/logos/polymarket.png",
+ "polymarket-us": "/logos/polymarket.png",
kalshi: "/logos/kalshi.jpg",
limitless: "/logos/limitless.png",
manifold: "/logos/manifold.svg",
From 7fad1cd33909b0792b1945415281787661ca0df5 Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 16:40:19 +0200
Subject: [PATCH 12/14] feat: dexpaprika product page + compact specialist
chain-chips + provider links
---
public/logos/dexpaprika.svg | 1 +
src/components/data-api-bench-groups.tsx | 48 ++++++++++++++++++++++--
src/data/provider-registry.ts | 17 +++++++++
src/lib/logo-manifest.ts | 1 +
4 files changed, 63 insertions(+), 4 deletions(-)
create mode 100644 public/logos/dexpaprika.svg
diff --git a/public/logos/dexpaprika.svg b/public/logos/dexpaprika.svg
new file mode 100644
index 00000000..122b7608
--- /dev/null
+++ b/public/logos/dexpaprika.svg
@@ -0,0 +1 @@
+LOGO_NMAsset 1
diff --git a/src/components/data-api-bench-groups.tsx b/src/components/data-api-bench-groups.tsx
index f2d07730..572a839a 100644
--- a/src/components/data-api-bench-groups.tsx
+++ b/src/components/data-api-bench-groups.tsx
@@ -105,7 +105,10 @@ function BenchRow({
{/* Leader */}
{bench.leader ? (
-
-
+
) : (
warming up
)}
@@ -133,7 +136,11 @@ function BenchRow({
{bench.runners.length > 0 ? (
bench.runners.map((r, i) => (
-
+
{i + 2}
@@ -147,7 +154,7 @@ function BenchRow({
>
{fmtDataValue(r.p50, bench.unit)}
-
+
))
) : (
...
@@ -223,6 +230,39 @@ function ChainStrip({
leaders: ChainLeader[];
unit: DataApiBenchRow["unit"];
}) {
+ const uniqueWinners = new Set(leaders.map((l) => l.providerSlug)).size;
+ const isSpecialist = uniqueWinners === leaders.length && leaders.length > 3;
+
+ if (isSpecialist) {
+ // Each chain has its own specialist — compact icon-only chips
+ const MAX = 5;
+ const visible = leaders.slice(0, MAX);
+ const overflow = leaders.length - MAX;
+ return (
+
+ {visible.map((l) => (
+
+ ))}
+ {overflow > 0 && (
+
+{overflow}
+ )}
+
+ );
+ }
+
+ // Some provider dominates multiple chains — full pills capped at 3
const MAX = 3;
const visible = leaders.slice(0, MAX);
const overflow = leaders.length - MAX;
diff --git a/src/data/provider-registry.ts b/src/data/provider-registry.ts
index 9174ff0f..99d7488f 100644
--- a/src/data/provider-registry.ts
+++ b/src/data/provider-registry.ts
@@ -115,6 +115,23 @@ export const PROVIDER_REGISTRY: Record
= {
"Independent crypto market data API. Token prices, OHLCV, exchange tickers, and contract/platform lookups across 300+ supported chains. Public free tier with no auth.",
twitter: "@coinpaprika",
},
+ dexpaprika: {
+ url: "https://dexpaprika.com",
+ description:
+ "DEX data API by the CoinPaprika team. REST endpoints for pools, OHLCV candles, trades, and token prices across 35+ blockchains. No auth required on the public tier.",
+ longDescription:
+ "DexPaprika is CoinPaprika's dedicated DEX pool indexer, built as a standalone product separate from the market-data API. It exposes REST endpoints for pool discovery, OHLCV candles, live trade feeds, and token prices across 35+ EVM and non-EVM chains. The public tier requires no API key, making it practical for prototyping and open-source tooling. Because DexPaprika is purpose-built for DEX data, its network list reflects actual pool-indexing depth rather than the broader chain coverage of the parent CoinPaprika market-data API.",
+ twitter: "@coinpaprika",
+ docs: "https://api.dexpaprika.com",
+ parent: "coinpaprika",
+ features: [
+ "Pool discovery and search across 35+ chains",
+ "OHLCV candles for any DEX pair",
+ "Live trade feed per pool",
+ "Token price derived from on-chain DEX data",
+ "No API key required on public tier",
+ ],
+ },
coinstats: {
url: "https://coinstats.app",
description:
diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts
index 5cdaf6a1..331da077 100644
--- a/src/lib/logo-manifest.ts
+++ b/src/lib/logo-manifest.ts
@@ -241,6 +241,7 @@ const RAW: Record = {
// ─── asset-registry-coverage + dex-network-coverage bench providers
// (bench № 005 split into 005a/005b) ───
coinpaprika: "/logos/coinpaprika.svg",
+ dexpaprika: "/logos/dexpaprika.svg",
coinstats: "/logos/coinstats.svg",
// ─── portfolio-chain-coverage bench providers (bench № 067) ───
From 3da3e439c9023d0ace286f63c2a5fff100231d0e Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 16:53:15 +0200
Subject: [PATCH 13/14] fix: leaderboard truncation line overflows footer when
9 rows shown
---
src/app/benchmarks/[slug]/share-card/route.tsx | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/app/benchmarks/[slug]/share-card/route.tsx b/src/app/benchmarks/[slug]/share-card/route.tsx
index 07abf13f..38b3576d 100644
--- a/src/app/benchmarks/[slug]/share-card/route.tsx
+++ b/src/app/benchmarks/[slug]/share-card/route.tsx
@@ -885,8 +885,12 @@ async function renderLeaderboard(
// the 630px canvas (weekend-drift 11 rows + long title case).
const count = sorted.length;
const titleLen = benchmark.title.length;
- const dense = count >= 8 || titleLen > 55;
- const veryDense = count >= 10 || titleLen > 70;
+ // The "and N more" line counts as an extra row for density — 9 rows +
+ // truncation line overflows the 630px canvas at dense (not veryDense)
+ // sizing, pushing the text on top of the CardFooter border.
+ const effectiveCount = count + (truncatedCount > 0 ? 1 : 0);
+ const dense = effectiveCount >= 8 || titleLen > 55;
+ const veryDense = effectiveCount >= 10 || titleLen > 70;
const titleSize = veryDense ? 32 : dense ? 40 : 50;
const rankSize = veryDense ? 18 : dense ? 20 : 24;
const nameSize = veryDense ? 18 : dense ? 20 : 24;
From 024e4825ebf65c34b62e2b4090ffd34e4763c366 Mon Sep 17 00:00:00 2001
From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com>
Date: Wed, 29 Jul 2026 16:58:14 +0200
Subject: [PATCH 14/14] fix: separate Region/Chain labels in breakdown col +
Data APIs footer
---
src/components/data-api-bench-groups.tsx | 14 ++++++++++----
src/components/site-footer.tsx | 1 +
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/src/components/data-api-bench-groups.tsx b/src/components/data-api-bench-groups.tsx
index 572a839a..59bb96f3 100644
--- a/src/components/data-api-bench-groups.tsx
+++ b/src/components/data-api-bench-groups.tsx
@@ -36,7 +36,7 @@ export function DataApiBenchGroups({ groups }: { groups: DataApiGroupRow[] }) {
Providers
Leader
Runners-up
- By region / chain
+ Breakdown
@@ -167,10 +167,16 @@ function BenchRow({
{showRegionChain ? (
{hasRegions && (
-
+
+ Region
+
+
)}
- {hasChains && !hasRegions && (
-
+ {hasChains && (
+
+ Chain
+
+
)}
) : (
diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx
index 4150e076..c094d91b 100644
--- a/src/components/site-footer.tsx
+++ b/src/components/site-footer.tsx
@@ -32,6 +32,7 @@ export function SiteFooter() {
{ label: "Chains", href: "/chains" },
{ label: "Prediction markets", href: "/prediction-markets" },
{ label: "RPC", href: "/rpc" },
+ { label: "Data APIs", href: "/data-api" },
{ label: "Perpetuals", href: "/perps" },
{ label: "Compare", href: "/compare" },
{ label: "Alternatives", href: "/alternatives" },