Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/app/rpc/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import { SITE } from "@/data/site";
*/

const DESCRIPTION =
"RPC providers ranked per chain from 3 regions: free public endpoints with their URLs, and Alchemy, Chainstack, QuickNode on API-key endpoints, ranked separately. Live 24h p50 latency.";
"RPC providers ranked per chain from 3 regions: free public endpoints with URLs, plus Alchemy, Chainstack and QuickNode on API-key endpoints, ranked apart.";

export const metadata: import("next").Metadata = pageMetadata({
path: "/rpc",
title: "RPC providers by chain: public endpoints and API-key providers ranked by latency",
title: "RPC providers by chain: public and API-key endpoints, by latency",
description: DESCRIPTION,
});

Expand Down Expand Up @@ -117,7 +117,7 @@ export default async function RpcHubPage() {
<header className="mb-8">
<p className="label-mono text-sky-600 mb-2">RPC nodes</p>
<h1 className="display text-4xl sm:text-5xl text-ink">
RPC providers by chain: public endpoints and API-key providers, ranked by latency
RPC providers by chain: public and API-key endpoints, ranked by latency
</h1>
<p className="mt-4 max-w-2xl text-base sm:text-lg text-ink-soft leading-snug">
Every free, no-key public RPC endpoint, measured per chain with
Expand Down Expand Up @@ -249,7 +249,7 @@ export default async function RpcHubPage() {
)}

<footer className="mt-16 pt-6 border-t border-ink/10 text-[12px] text-ink-soft leading-relaxed">
<p className="label-mono text-ink-faint mb-2">Methodology</p>
<h2 className="label-mono text-ink-faint mb-2">Methodology</h2>
<p>
Each chain row aggregates that chain&apos;s dedicated bench: an
identical JSON-RPC POST (<code>eth_getBlockByNumber(&quot;latest&quot;, false)</code> with
Expand Down
2 changes: 1 addition & 1 deletion src/components/rpc-chains-leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export function RpcChainsLeaderboard({
dir={sortDir}
onClick={() => setSort("bestP90")}
>
<span title="p90 latency of the same overall-best provider — the slow-tail number users actually feel. Empty when the provider's histogram doesn't have enough samples yet.">
<span title="p90 latency of the same overall-best provider, the slow-tail number users actually feel. Empty when the provider's histogram doesn't have enough samples yet.">
Best p90
</span>
</ThSort>
Expand Down
35 changes: 27 additions & 8 deletions src/lib/related-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,37 @@ function isLiveAppearance(a: ProviderAppearance): boolean {
return a.result.availability !== "unavailable" && a.result.ms.p50 > 0;
}

/** Two appearances on the same bench are comparable only in the same
* access cohort: on a chain RPC page a public gateway and a keyed
* provider are never ranked together, and the compare page
* (sharedBenchSlugs) refuses the pair, so the link must too. */
function sameCohort(a: ProviderAppearance, b: ProviderAppearance): boolean {
return (a.tier ?? null) === (b.tier ?? null);
}

/** Benches both providers appear on, same cohort only (live or not). */
export function sharedBenchCount(
a: ProviderAppearance[],
b: ProviderAppearance[],
): number {
const aByBench = new Map(a.map((x) => [x.benchmark.slug, x] as const));
let n = 0;
for (const x of b) {
const mine = aByBench.get(x.benchmark.slug);
if (mine && sameCohort(mine, x)) n += 1;
}
return n;
}

export function liveSharedBenchCount(
a: ProviderAppearance[],
b: ProviderAppearance[],
): number {
const aLive = new Set(a.filter(isLiveAppearance).map((x) => x.benchmark.slug));
const aLive = new Map(a.filter(isLiveAppearance).map((x) => [x.benchmark.slug, x] as const));
let n = 0;
for (const x of b) {
if (isLiveAppearance(x) && aLive.has(x.benchmark.slug)) n += 1;
const mine = aLive.get(x.benchmark.slug);
if (isLiveAppearance(x) && mine && sameCohort(mine, x)) n += 1;
}
return n;
}
Expand Down Expand Up @@ -105,17 +128,13 @@ export const getCompareCandidates = cache(async function getCompareCandidates(
const me = providerSlug.toLowerCase();
const meProfile = profiles.find((p) => p.slug.toLowerCase() === me);
if (!meProfile) return [];
const myBenches = new Set(meProfile.appearances.map((a) => a.benchmark.slug));
if (myBenches.size === 0) return [];
if (meProfile.appearances.length === 0) return [];

const out: CompareCandidate[] = [];
for (const other of profiles) {
if (other.slug.toLowerCase() === me) continue;
if (DEAD_COMPOSITE_SLUGS.has(other.slug.toLowerCase())) continue;
let shared = 0;
for (const a of other.appearances) {
if (myBenches.has(a.benchmark.slug)) shared += 1;
}
const shared = sharedBenchCount(meProfile.appearances, other.appearances);
if (shared === 0) continue;
const pairSlug = canonicalPairSlug(meProfile.slug, other.slug);
// Only pairs whose compare page is indexable (see isPairLinkable).
Expand Down
Loading