diff --git a/src/app/bridge/page.tsx b/src/app/bridge/page.tsx
new file mode 100644
index 00000000..53ca651a
--- /dev/null
+++ b/src/app/bridge/page.tsx
@@ -0,0 +1,485 @@
+import Link from "next/link";
+import { fetchBridgeHub } from "@/lib/bridge-hub-stats";
+import type { BridgeProviderRow } from "@/lib/bridge-hub-stats";
+import { pageMetadata } from "@/lib/page-metadata";
+import { safeJsonLd, buildBreadcrumbJsonLd } from "@/lib/jsonld";
+import { SITE } from "@/data/site";
+
+const DESCRIPTION =
+ "Live cross-chain bridge benchmarks: cheapest all-in fee and fastest quote API for $300 USDC across Across, deBridge, LI.FI, Mobula, Near Intents and Relay. 4 corridors, refreshed every 5 minutes.";
+
+export const metadata: import("next").Metadata = pageMetadata({
+ path: "/bridge",
+ title: "Cheapest cross-chain bridge 2026, live ranking",
+ description: DESCRIPTION,
+});
+
+export const revalidate = 60;
+
+export default async function BridgeHubPage() {
+ const hub = await fetchBridgeHub();
+
+ const breadcrumbLd = {
+ "@context": "https://schema.org",
+ ...buildBreadcrumbJsonLd([
+ { name: "Home", item: SITE.url },
+ { name: "Bridge benchmarks", item: `${SITE.url}/bridge` },
+ ]),
+ };
+
+ const itemListLd = hub
+ ? {
+ "@context": "https://schema.org",
+ "@type": "ItemList",
+ name: "Cross-chain bridge benchmarks by OpenChainBench",
+ description: DESCRIPTION,
+ numberOfItems: hub.bridgeCount,
+ itemListElement: hub.providers.map((p, i) => ({
+ "@type": "ListItem",
+ position: i + 1,
+ name: p.name,
+ url: `${SITE.url}/products/${p.slug}`,
+ })),
+ }
+ : null;
+
+ const byFee = hub?.providers ?? [];
+
+ const bySpeed = hub
+ ? [...hub.providers]
+ .filter((p) => p.quotep50 != null)
+ .sort((a, b) => (a.quotep50 ?? Infinity) - (b.quotep50 ?? Infinity))
+ : [];
+
+ return (
+
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: serialized via safeJsonLd */}
+
+ {itemListLd && (
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: serialized via safeJsonLd
+
+ )}
+
+
+ Bridges
+
+ Cross-chain bridge benchmarks, live.
+
+
+ Two live benches, sampled every five minutes from eu-west. One
+ measures the all-in cost of bridging $300 USDC across four
+ Solana/Base/Arbitrum/HyperCore corridors. The other measures
+ how fast each bridge returns a usable quote. Same corridors,
+ same notional, same origin.
+
+
+
+
+ Bench 003
+
+ bridge-fee
+
+
+
+ Bench 002
+
+ bridge-quote-latency
+
+
+ eu-west only
+
+
+
+
+ {hub ? (
+ <>
+
+
+
+
+
+ Cost ranking
+
+
+ per-corridor breakdown
+
+
+
+ All-in fee at $300 USDC (fees + slippage + destination gas) as
+ % of notional. p50 over 24h, cross-corridor average. Lower is
+ better.
+
+
+
+
+
+
+
+ Quote speed ranking
+
+
+ per-corridor breakdown
+
+
+
+ Time to receive a usable quote, wall-clock ms. p50 over 24h,
+ cross-corridor average. Lower is better.
+
+
+
+
+
+
+ Three pricing architectures
+
+
+
+
+
+
+ Corridors measured
+
+
+ {hub.corridors.map((c) => (
+
+ {c.label}
+
+ ))}
+
+
+ Per-corridor leaders can diverge from the cross-corridor
+ aggregate. Full corridor tabs with p50/p90/p99 splits are on
+ the{" "}
+
+ bridge-fee
+ {" "}
+ and{" "}
+
+ bridge-quote-latency
+ {" "}
+ bench pages.
+
+
+ >
+ ) : (
+
+
+ Data warming up
+
+
+ The bridge snapshot has not been published yet. Individual bench
+ pages are already live:
+
+
+
+
+ bridge-fee
+
+
+
+
+ bridge-quote-latency
+
+
+
+
+ )}
+
+
+ Methodology
+
+ Cost rows:{" "}
+ bridge_cost_percent (all-in: fees + slippage +
+ destination gas) at $300 USDC, p50 over the trailing 24h
+ averaged across corridors each provider supports. Quote latency
+ rows: wall-clock time from request send to last byte received,
+ p50 over the trailing 24h via{" "}
+
+ histogram_quantile(0.50, sum by (le)
+ (rate(bridge_quote_latency_ms_bucket[24h])))
+
+ . Both benches run from eu-west only; multi-region expansion
+ requires additional harness instances. Failures (quote_failed,
+ unsupported route, timeout) are excluded from cost and latency
+ aggregates and counted toward success rate.
+
+
+ Data and methodology released under{" "}
+
+ CC BY 4.0
+
+ . Reuse with attribution to OpenChainBench.
+
+
+
+ );
+}
+
+function SummaryCard({
+ label,
+ value,
+ accent,
+ tip,
+}: {
+ label: string;
+ value: string;
+ accent?: string;
+ tip?: string;
+}) {
+ return (
+
+
+ {accent && (
+
+ )}
+ {label}
+
+
+ {value}
+
+
+ );
+}
+
+function BridgeTable({
+ rows,
+ metric,
+}: {
+ rows: BridgeProviderRow[];
+ metric: "fee" | "latency";
+}) {
+ if (rows.length === 0) return null;
+ return (
+
+
+
+
+
+ #
+
+
+ Provider
+
+
+ Type
+
+ {metric === "fee" ? (
+ <>
+
+ p50 fee
+
+
+ p99 fee
+
+
+ Success
+
+ >
+ ) : (
+ <>
+
+ p50 quote
+
+
+ p99 quote
+
+
+ Success
+
+ >
+ )}
+
+
+
+ {rows.map((row, i) => {
+ const p50 = metric === "fee" ? row.feep50 : row.quotep50;
+ const p99 = metric === "fee" ? row.feep99 : row.quotep99;
+ const success =
+ metric === "fee" ? row.feeSuccess : row.quoteSuccess;
+ const fmt = metric === "fee" ? fmtPct : fmtMs;
+ return (
+
+
+ {i + 1}
+
+
+
+ {row.name}
+
+
+
+ {row.type && (
+
+ {TYPE_LABELS[row.type] ?? row.type}
+
+ )}
+
+
+ {p50 != null ? fmt(p50) : — }
+
+
+ {p99 != null ? fmt(p99) : — }
+
+
+ {success != null ? fmtSuccess(success) : — }
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+function ArchCard({
+ label,
+ examples,
+ body,
+}: {
+ label: string;
+ examples: string[];
+ body: string;
+}) {
+ return (
+
+
{label}
+
{examples.join(", ")}
+
{body}
+
+ );
+}
+
+const TYPE_LABELS: Record = {
+ intent: "Intent layer",
+ relay: "Relay",
+ aggregator: "Aggregator",
+ protocol: "Protocol",
+};
+
+function fmtPct(v: number): string {
+ if (!Number.isFinite(v)) return "...";
+ return `${v.toFixed(2)}%`;
+}
+
+function fmtMs(v: number): string {
+ if (!Number.isFinite(v)) return "...";
+ if (v < 1000) return `${Math.round(v)} ms`;
+ return `${(v / 1000).toFixed(2)} s`;
+}
+
+function fmtSuccess(v: number): string {
+ if (!Number.isFinite(v)) return "...";
+ return `${(v * 100).toFixed(0)}%`;
+}
diff --git a/src/lib/bridge-hub-stats.ts b/src/lib/bridge-hub-stats.ts
new file mode 100644
index 00000000..8500a879
--- /dev/null
+++ b/src/lib/bridge-hub-stats.ts
@@ -0,0 +1,113 @@
+/**
+ * Server-side helper for the /bridge hub page. Loads both bridge bench
+ * blobs (bridge-fee and bridge-quote-latency) from the CDN and merges
+ * them into a unified per-provider shape.
+ *
+ * No Prometheus queries at render time. Both benches are already written
+ * by the materialise worker, so this is two blob fetches and a join.
+ * Degrades gracefully: if only one blob is available, the hub renders
+ * whatever columns it has and marks the others null.
+ */
+
+import { unstable_cache } from "next/cache";
+import { loadBenchFromBlob } from "@/lib/bench-blob";
+import type { ProviderType } from "@/types/benchmark";
+
+export type BridgeProviderRow = {
+ slug: string;
+ name: string;
+ type: ProviderType | undefined;
+ tag: string | undefined;
+ feep50: number | null;
+ feep99: number | null;
+ feeSuccess: number | null;
+ quotep50: number | null;
+ quotep99: number | null;
+ quoteSuccess: number | null;
+};
+
+export type BridgeHubData = {
+ providers: BridgeProviderRow[];
+ corridors: { value: string; label: string }[];
+ cheapestSlug: string | null;
+ cheapestName: string | null;
+ cheapestP50: number | null;
+ fastestSlug: string | null;
+ fastestName: string | null;
+ fastestP50: number | null;
+ bridgeCount: number;
+};
+
+async function _fetchBridgeHub(): Promise {
+ const [feeBench, latencyBench] = await Promise.all([
+ loadBenchFromBlob("bridge-fee"),
+ loadBenchFromBlob("bridge-quote-latency"),
+ ]);
+
+ if (!feeBench && !latencyBench) return null;
+
+ const slugSet = new Set();
+ for (const r of feeBench?.results ?? []) slugSet.add(r.slug);
+ for (const r of latencyBench?.results ?? []) slugSet.add(r.slug);
+
+ const feeBySlug = new Map(
+ (feeBench?.results ?? []).map((r) => [r.slug, r]),
+ );
+ const latBySlug = new Map(
+ (latencyBench?.results ?? []).map((r) => [r.slug, r]),
+ );
+
+ const providers: BridgeProviderRow[] = [];
+ for (const slug of slugSet) {
+ const feeRow = feeBySlug.get(slug);
+ const latRow = latBySlug.get(slug);
+ const base = feeRow ?? latRow!;
+ const feeAlive = feeRow?.availability !== "unavailable";
+ const latAlive = latRow?.availability !== "unavailable";
+ providers.push({
+ slug,
+ name: base.name,
+ type: base.type,
+ tag: base.tag,
+ feep50: feeAlive ? (feeRow?.ms.p50 ?? null) : null,
+ feep99: feeAlive ? (feeRow?.ms.p99 ?? null) : null,
+ feeSuccess: feeRow?.successRate ?? null,
+ quotep50: latAlive ? (latRow?.ms.p50 ?? null) : null,
+ quotep99: latAlive ? (latRow?.ms.p99 ?? null) : null,
+ quoteSuccess: latRow?.successRate ?? null,
+ });
+ }
+
+ // Sort by fee p50 asc (lower = cheaper), nulls last
+ providers.sort(
+ (a, b) => (a.feep50 ?? Infinity) - (b.feep50 ?? Infinity),
+ );
+
+ const byQuote = [...providers]
+ .filter((p) => p.quotep50 != null)
+ .sort((a, b) => (a.quotep50 ?? Infinity) - (b.quotep50 ?? Infinity));
+
+ const cheapest = providers.find((p) => p.feep50 != null) ?? null;
+ const fastest = byQuote[0] ?? null;
+
+ const corridors =
+ feeBench?.dimensions?.chain ?? latencyBench?.dimensions?.chain ?? [];
+
+ return {
+ providers,
+ corridors,
+ cheapestSlug: cheapest?.slug ?? null,
+ cheapestName: cheapest?.name ?? null,
+ cheapestP50: cheapest?.feep50 ?? null,
+ fastestSlug: fastest?.slug ?? null,
+ fastestName: fastest?.name ?? null,
+ fastestP50: fastest?.quotep50 ?? null,
+ bridgeCount: slugSet.size,
+ };
+}
+
+export const fetchBridgeHub = unstable_cache(
+ _fetchBridgeHub,
+ ["bridge-hub"],
+ { revalidate: 60 },
+);
diff --git a/src/lib/sitemap-builder.ts b/src/lib/sitemap-builder.ts
index 67a158e4..c700a10e 100644
--- a/src/lib/sitemap-builder.ts
+++ b/src/lib/sitemap-builder.ts
@@ -116,6 +116,7 @@ function staticHubRoutes(catalogTs: Date): MetadataRoute.Sitemap {
{ url: `${SITE.url}/prediction-markets`, lastModified: catalogTs, changeFrequency: "hourly", priority: 0.9 },
{ url: `${SITE.url}/rpc`, lastModified: catalogTs, changeFrequency: "hourly", priority: 0.9 },
{ url: `${SITE.url}/perps`, lastModified: catalogTs, changeFrequency: "hourly", priority: 0.9 },
+ { url: `${SITE.url}/bridge`, lastModified: catalogTs, changeFrequency: "hourly", priority: 0.9 },
{ url: `${SITE.url}/mcp`, lastModified: pageMtime("mcp/page.tsx"), changeFrequency: "monthly", priority: 0.8 },
{ url: `${SITE.url}/methodology`, lastModified: pageMtime("methodology/page.tsx"), changeFrequency: "monthly", priority: 0.7 },
{ url: `${SITE.url}/contribute`, lastModified: pageMtime("contribute/page.tsx"), changeFrequency: "monthly", priority: 0.7 },