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
14 changes: 7 additions & 7 deletions benchmarks/token-quote-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ per_chain_explainer:
- slug: base
h2: "Best DEX aggregator for Base launchpad token coverage"
body: |
{{best_name:chain:base}} currently leads on Base at {{best_p50:chain:base}} coverage (24h) across Virtuals Protocol. Base launchpad graduates from Virtuals converge on Uniswap V3 pools, so EVM aggregators with a live pool-creation event index have the narrowest discovery lag. The practical differentiator is indexing latency: an aggregator scraping the Uniswap V3 Factory log in real time picks up a new token within one block; one relying on a database sync job may lag by hours, recording misses on tokens the market is already actively trading. Virtuals agent tokens show wider spread across providers due to metadata-gating in some routing engines.
{{best_name:chain:base}} currently leads on Base at {{best_p50:chain:base}} coverage (24h). Virtuals Protocol has expanded its presence across chains: newer Virtuals agents now graduate to Robinhood Chain (chainId 4663) rather than Base. EVM aggregators with a live Uniswap V3 pool-creation event index have the narrowest discovery lag on Base-native tokens.
- slug: bsc
h2: "Best DEX aggregator for BNB Chain new token coverage"
body: |
Expand Down Expand Up @@ -68,15 +68,15 @@ abstract: |
structural (it does not index Meteora DBC pool types) or temporary
(it lags new Raydium pairs by a few minutes). The chain dimension
narrows to a single chain for providers that are chain-specific
(Jupiter is Solana-only; ParaSwap covers Base and BNB only; KyberSwap, Mobula,
(Jupiter is Solana-only; ParaSwap covers Base, BNB and Robinhood; KyberSwap, Mobula,
and Relay cover all four chains).

methodology:
- "Token discovery: five sources in parallel. (1) Dexscreener token-boosts: pump-fun (Solana) and pons (Robinhood). (2) Virtuals Protocol API: Base graduated agents (virtuals). (3) GeckoTerminal BSC new_pools: four-meme. (4) GeckoTerminal Solana moonshot/pools: moonshot. (5) GeckoTerminal Solana meteora-dbc/pools: meteora-dbc. Boosted tokens from (1) are enriched via Dexscreener batch lookup to resolve dexId to venue labels."
- "Token discovery: five sources in parallel. (1) Dexscreener token-boosts: pump-fun (Solana) and pons (Robinhood). (2) Virtuals Protocol API: Virtuals agents on Base and Robinhood chain (virtuals). (3) GeckoTerminal BSC new_pools: four-meme. (4) GeckoTerminal Solana moonshot/pools: moonshot. (5) GeckoTerminal Solana meteora-dbc/pools: meteora-dbc. Boosted tokens from (1) are enriched via Dexscreener batch lookup to resolve dexId to venue labels."
- "Quote probe: 1 USDC → token, fired in parallel to all in-scope providers with a 10s timeout. Input token is always USDC (native USDC on Solana and Base, bridged USDC.e on BNB). A probe is a coverage hit when the HTTP response is 2xx and the parsed outAmount field is a non-zero positive integer or decimal. All other outcomes (4xx, 5xx, no-route JSON, empty outAmount, timeout) record as misses."
- "Aggregator endpoints: Jupiter lite-api.jup.ag/swap/v1/quote (Solana), KyberSwap aggregator-api.kyberswap.com/{chain}/api/v1/routes (EVM), ParaSwap apiv5.paraswap.io/prices (EVM), Mobula /api/2/swap/quoting (EVM + Solana, API key), Relay api.relay.link/quote (EVM + Solana, no key). Each adapter normalises chain identifiers, input/output decimals, and error response shapes before recording outcome."
- "Jupiter strictMode=false: queried without the curated-token filter so Jupiter will attempt routing for any token with a discoverable on-chain pool, matching how most wallets call the API. strictMode=true would trivially exclude new tokens and defeat the purpose of the benchmark."
- "Chain-scope enforcement: Jupiter probes Solana-only. ParaSwap probes Base and BNB only. KyberSwap probes Base, BNB, and Robinhood. Mobula and Relay probe all four chains (Solana, Base, BNB, Robinhood). Providers are never penalised for chains they do not support: null cells are excluded from cross-chain aggregates."
- "Chain-scope enforcement: Jupiter probes Solana-only. ParaSwap probes Base, BNB and Robinhood. KyberSwap probes Base, BNB, and Robinhood. Mobula and Relay probe all four chains (Solana, Base, BNB, Robinhood). Providers are never penalised for chains they do not support: null cells are excluded from cross-chain aggregates."
- "Coverage metric: Prometheus counters token_quote_coverage_success_total and token_quote_coverage_attempts_total carry labels {provider, venue, chain}. The bench queries compute increase(success[24h]) / increase(attempts[24h]) * 100 live at read time. No pre-aggregation; the ratio reflects exactly the tokens probed in the rolling window."
- "Probe cadence and deduplication: each batch cycles through the current Dexscreener token-boosts feed. Tokens are not deduplicated across batches; the same token may appear in multiple 30-minute windows if it remains boosted. The score therefore reflects freshness of the aggregator's current routing graph, not historical coverage depth."
- "Single origin: all probes originate from the eu-west OCB VPS. Regional latency is not a factor in this bench (the outcome is binary hit/miss, not latency). No region dimension in v1."
Expand All @@ -103,7 +103,7 @@ faq:
- q: "Why is Jupiter queried with strictMode=false?"
a: "Jupiter's strictMode=true restricts quotes to verified tokens on its curated list, which defeats the purpose of a coverage bench targeting newly-launched tokens. strictMode=false instructs Jupiter to attempt routing for any token with an existing pool, matching how wallets typically call the API when displaying all tradeable tokens."
- q: "What happens when a provider is chain-specific?"
a: "Jupiter is Solana-only and appears as null on EVM rows. ParaSwap covers Base and BNB only. KyberSwap covers Base, BNB, and Robinhood. Mobula and Relay cover all four chains including Solana. Null cells are excluded from cross-chain aggregates so chain-restricted providers are not penalized."
a: "Jupiter is Solana-only and appears as null on EVM rows. ParaSwap covers Base, BNB, and Robinhood. KyberSwap covers Base, BNB, and Robinhood. Mobula and Relay cover all four chains including Solana. Null cells are excluded from cross-chain aggregates so chain-restricted providers are not penalized."

source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/token-quote-coverage

Expand Down Expand Up @@ -180,8 +180,8 @@ providers:

- slug: paraswap
name: ParaSwap
tag: EVM multi-chain, Base + BNB
formula: "Rolling 24h ratio for provider=paraswap. EVM launchpads only (Base, BNB). Solana and Robinhood cells null."
tag: EVM multi-chain, Base + BNB + Robinhood
formula: "Rolling 24h ratio for provider=paraswap. EVM launchpads only (Base, BNB, Robinhood). Solana cells null."
queries:
p50: sum(increase(token_quote_coverage_success_total{provider="paraswap"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="paraswap"}[24h])) * 100
p90: sum(increase(token_quote_coverage_attempts_total{provider="paraswap"}[24h]))
Expand Down
10 changes: 7 additions & 3 deletions harnesses/token-quote-coverage/cmd/monitor/paraswap.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (
)

const (
paraswapUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
paraswapUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"
paraswapUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
paraswapUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"
paraswapUSDGRobinhood = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
)

type ParaSwapProvider struct {
Expand All @@ -25,7 +26,7 @@ func NewParaSwapProvider() *ParaSwapProvider {
func (p *ParaSwapProvider) Slug() string { return "paraswap" }

func (p *ParaSwapProvider) SupportsChain(chain string) bool {
return chain == "base" || chain == "bsc"
return chain == "base" || chain == "bsc" || chain == "robinhood"
}

func (p *ParaSwapProvider) Quote(ctx context.Context, token Token) (ok bool) {
Expand All @@ -37,6 +38,9 @@ func (p *ParaSwapProvider) Quote(ctx context.Context, token Token) (ok bool) {
case "bsc":
network = "56"
usdcAddr = paraswapUSDCBSC
case "robinhood":
network = "4663"
usdcAddr = paraswapUSDGRobinhood
default:
RecordProbe(p.Slug(), token.Venue, token.Chain, false)
return false
Expand Down
25 changes: 25 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,31 @@ const nextConfig: NextConfig = {
},
],
},
{
// Sitemap: force-dynamic in the route file (Data Cache 2MB cap
// blows past for the 500+ URL corpus), so nothing caches it by
// default and every crawler hit re-runs the full loader chain.
// Hold it edge-side for an hour with SWR so cold hits stay fast.
source: "/sitemap.xml",
headers: [
{
key: "Cache-Control",
value: "public, s-maxage=3600, stale-while-revalidate=86400",
},
],
},
{
// Citable JSON is polled by LLM crawlers (Perplexity, ChatGPT,
// Claude Deep Research) — a bare `public` with no s-maxage sent
// every scrape to origin. Same freshness window as sitemap.
source: "/api/citable",
headers: [
{
key: "Cache-Control",
value: "public, s-maxage=3600, stale-while-revalidate=86400",
},
],
},
];
},
async redirects() {
Expand Down
43 changes: 11 additions & 32 deletions src/app/benchmarks/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,17 @@ export async function generateStaticParams() {
return [];
}

// Emit one OG image per (slug, chain) combo so social shares of
// `/benchmarks/{slug}?chain=X` render a chain-honest card instead of the
// cross-chain aggregate leader (which is the misleading baseline-skew
// case bench-001 ran into: GMGN looking like "fastest crypto data API"
// on the unfiltered view because Solana's faster baseline drags the
// average down). The `id` Next.js threads through to the default image
// handler is the chain value; the unfiltered card uses `id="default"`.
export async function generateImageMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// Load editorial-only via getBenchmark (memoised; safe at build time).
const b = await getBenchmark(slug);
const chains = (b?.dimensions?.chain ?? []).filter(
(c) => c.value !== "all",
);
return [
{
id: "default",
alt,
size,
contentType,
},
...chains.map((c) => ({
id: c.value,
alt: `${alt}. ${c.label}`,
size,
contentType,
})),
];
// Emit a SINGLE OG image for the aggregate bench page. Previously we
// fanned out one entry per chain variant so `/benchmarks/{slug}?chain=X`
// social shares could render a chain-honest card, but Next hoists every
// generateImageMetadata entry into a separate <meta property="og:image">
// on the parent page — X, LinkedIn, Slack pick nondeterministically
// among them, so a share of `?chain=solana` might render the `robinhood`
// card. Chain-scoped pages live at `/benchmarks/[slug]/[chain]` and can
// carry their own opengraph-image handler if per-chain cards become a
// priority; today they inherit the site-wide default OG.
export async function generateImageMetadata() {
return [{ id: "default", alt, size, contentType }];
}

export default async function OG({
Expand Down
12 changes: 11 additions & 1 deletion src/app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ import { ArrowLeft } from "lucide-react";

export const metadata: Metadata = {
title: "Page not found",
robots: { index: false },
// Full robots shape so Next replaces (not merges) the layout's robots
// meta. Previous shape `{ index: false }` caused two `<meta name="robots"
// content="noindex">` tags on prod 404s (layout emitted one from its
// own defaults, this route emitted a second) — Bing had been observed
// to ignore the duplicated tag and fall through to the layout's
// index=true. Match the shape used by the layout's IS_STAGING branch.
robots: {
index: false,
follow: false,
googleBot: { index: false, follow: false },
},
};

export default function NotFound() {
Expand Down
16 changes: 16 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ const COMPARE_PATH = /^\/compare\/([a-z0-9][a-z0-9-]{0,79})\/?$/;
export function middleware(req: NextRequest) {
const { pathname, search } = req.nextUrl;

// Case normalisation. `/products/Alchemy` and `/Benchmarks/foo` used
// to serve 200 with the mixed-case URL while the HTML canonical
// pointed at the lowercase form — Google indexed the mixed-case URL
// as a valid variant and burned crawl budget on both. Force lowercase
// via 308 so only the canonical shape reaches origin.
if (pathname !== pathname.toLowerCase()) {
const url = req.nextUrl.clone();
url.pathname = pathname.toLowerCase();
return NextResponse.redirect(url, 308);
}

if (search && CANONICAL_NO_QUERY.has(pathname)) {
const canonical = req.nextUrl.clone();
canonical.search = "";
Expand Down Expand Up @@ -136,6 +147,11 @@ export const config = {
"/api/llm-context",
"/api/freshness",
"/api/openapi.json",
// Wildcard `/((?!_next|.*\..*).*)` covers every HTML route so the
// case-normalisation redirect fires on any URL, not just the
// enumerated bench/answer/compare/product paths. Excludes _next
// internals and any file with an extension (assets, RSS, sitemap).
"/((?!_next|api|.*\\..*).*)",
"/benchmarks/:slug*",
"/answers/:slug*",
"/compare/:slug*",
Expand Down
Loading