diff --git a/benchmarks/token-quote-coverage.yml b/benchmarks/token-quote-coverage.yml index d766c819..c4753218 100644 --- a/benchmarks/token-quote-coverage.yml +++ b/benchmarks/token-quote-coverage.yml @@ -19,7 +19,7 @@ per_chain_explainer: - slug: bnb h2: "Best DEX aggregator for BNB Chain new token coverage" body: | - {{best_name:chain:bnb}} currently leads on BNB Chain at {{best_p50:chain:bnb}} coverage (24h) across Four.meme. BNB is the hardest environment in this benchmark for non-native aggregators: Four.meme graduates to PancakeSwap V2, and routing engines that do not maintain a live BNB Chain pool-creation index miss newly-minted pairs entirely. Providers that derive their BNB token list from a shared coingecko-style database rather than on-chain event tracking typically lag by several hours. The probe fires a 1 USDC (via USDC.e bridged) to token quote; BNB-native aggregators (OKX DEX, KyberSwap) have structural coverage advantages over Solana-first providers here. + {{best_name:chain:bnb}} currently leads on BNB Chain at {{best_p50:chain:bnb}} coverage (24h) across Four.meme. BNB is the hardest environment in this benchmark for non-native aggregators: Four.meme graduates to PancakeSwap V2, and routing engines that do not maintain a live BNB Chain pool-creation index miss newly-minted pairs entirely. Providers that derive their BNB token list from a shared coingecko-style database rather than on-chain event tracking typically lag by several hours. The probe fires a 1 USDC (via USDC.e bridged) to token quote; KyberSwap has structural coverage advantages over Solana-first providers here due to its live BNB pool-creation event index. category: Aggregators status: draft @@ -45,7 +45,6 @@ seo_intro: | a live Base pool graph. Four.meme graduates need PancakeSwap v2 event indexing. An aggregator that dominates one launchpad can have gaps on another depending on how it discovers new pools. - Current leader overall is {{best_name}} at {{best_p50}}. abstract: | We benchmark DEX aggregators on their ability to quote tokens that @@ -65,15 +64,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; 1inch and KyberSwap are EVM-only on the + (Jupiter is Solana-only; KyberSwap and Odos are EVM-only on the launchpads they cover). methodology: - "Token discovery: Dexscreener /token-boosts/latest/v1 returns recently-active tokens sorted by on-chain boost activity. Up to 20 tokens per 30-minute batch are selected from each supported chain (Solana, Base, BNB). A follow-up batch call to /latest/dex/tokens/{addresses} resolves the originating dexId (pool factory), which maps to a venue label: pumpfun/moonshot/meteora-dlmm→Solana venues, virtual-fun/clanker/wow→Base venues, any BNB pair→four-meme." - "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: Mobula /api/1/trade/swap/quote (Bearer auth, multi-chain), Jupiter quote-api.jup.ag/v6/quote?strictMode=false (Solana), KyberSwap aggregator-api.kyberswap.com/{chain}/api/v1/routes (EVM), OKX DEX /api/v5/dex/aggregator/quote (multi-chain), Odos /sor/quote/v2 (EVM). Each adapter normalises chain identifiers, input/output decimals, and error response shapes before recording outcome." + - "Aggregator endpoints: Mobula /api/2/swap/quoting (Bearer auth, EVM), Jupiter lite-api.jup.ag/swap/v1/quote (Solana), KyberSwap aggregator-api.kyberswap.com/{chain}/api/v1/routes (EVM), Odos /sor/quote/v2 (EVM). 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; KyberSwap and Odos probe EVM chains only (Base, BNB). OKX DEX and Mobula probe all three chains. Providers are never penalised for chains they do not support: Solana cells for KyberSwap and Odos are null, not zero, and are excluded from cross-chain aggregates." + - "Chain-scope enforcement: Jupiter probes Solana-only; KyberSwap, Odos, and Mobula probe EVM chains only (Base, BNB). Providers are never penalised for chains they do not support: Solana cells for KyberSwap, Odos, and Mobula are null, not zero, and 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." @@ -100,7 +99,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 with null values on Base and BNB launchpad rows. 1inch and KyberSwap are EVM-only and appear as null on Solana launchpad rows. OKX DEX and Mobula cover multiple chains and are probed on all launchpads they support. Null-chain cells are excluded from cross-launchpad aggregates so chain-specific providers are not penalized." + a: "Jupiter is Solana-only and appears with null values on Base and BNB launchpad rows. KyberSwap, Odos, and Mobula are EVM-only and appear as null on Solana launchpad rows. Null-chain cells are excluded from cross-launchpad aggregates so chain-specific providers are not penalized." source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/token-quote-coverage @@ -175,19 +174,6 @@ providers: sample_size: sum(increase(token_quote_coverage_attempts_total{provider="kyberswap"}[24h])) series: sum(rate(token_quote_coverage_success_total{provider="kyberswap"}[1h])) / sum(rate(token_quote_coverage_attempts_total{provider="kyberswap"}[1h])) * 100 - - slug: okx-dex - name: OKX DEX - tag: Multi-chain, Solana + EVM - formula: "Rolling 24h ratio for provider=okx-dex. Covers Solana and EVM chains." - queries: - p50: sum(increase(token_quote_coverage_success_total{provider="okx-dex"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="okx-dex"}[24h])) * 100 - p90: sum(increase(token_quote_coverage_attempts_total{provider="okx-dex"}[24h])) - p99: sum(increase(token_quote_coverage_success_total{provider="okx-dex"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="okx-dex"}[24h])) * 100 - mean: sum(increase(token_quote_coverage_success_total{provider="okx-dex"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="okx-dex"}[24h])) * 100 - success: clamp_max(sum(increase(token_quote_coverage_success_total{provider="okx-dex"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="okx-dex"}[24h])), 1) - sample_size: sum(increase(token_quote_coverage_attempts_total{provider="okx-dex"}[24h])) - series: sum(rate(token_quote_coverage_success_total{provider="okx-dex"}[1h])) / sum(rate(token_quote_coverage_attempts_total{provider="okx-dex"}[1h])) * 100 - - slug: odos name: Odos tag: EVM multi-chain, Base + BNB + Ethereum diff --git a/harnesses/token-quote-coverage/.env.example b/harnesses/token-quote-coverage/.env.example index 05932995..21e98e19 100644 --- a/harnesses/token-quote-coverage/.env.example +++ b/harnesses/token-quote-coverage/.env.example @@ -1,4 +1,3 @@ MONITOR_REGION=eu-west MOBULA_API_KEY=your_key -OKX_DEX_API_KEY=optional LOGS_TOKEN=optional_for_logs_endpoint diff --git a/harnesses/token-quote-coverage/cmd/monitor/config.go b/harnesses/token-quote-coverage/cmd/monitor/config.go index de223060..b66ba57a 100644 --- a/harnesses/token-quote-coverage/cmd/monitor/config.go +++ b/harnesses/token-quote-coverage/cmd/monitor/config.go @@ -11,8 +11,7 @@ import ( type Config struct { MonitorRegion string LogsToken string - MobulaAPIKey string - OKXDEXAPIKey string // optional + MobulaAPIKey string } func loadEnv() (*Config, error) { @@ -21,8 +20,7 @@ func loadEnv() (*Config, error) { c := &Config{ MonitorRegion: strings.TrimSpace(os.Getenv("MONITOR_REGION")), LogsToken: strings.TrimSpace(os.Getenv("LOGS_TOKEN")), - MobulaAPIKey: strings.TrimSpace(os.Getenv("MOBULA_API_KEY")), - OKXDEXAPIKey: strings.TrimSpace(os.Getenv("OKX_DEX_API_KEY")), + MobulaAPIKey: strings.TrimSpace(os.Getenv("MOBULA_API_KEY")), } if c.MonitorRegion == "" { c.MonitorRegion = "eu-west" diff --git a/harnesses/token-quote-coverage/cmd/monitor/main.go b/harnesses/token-quote-coverage/cmd/monitor/main.go index b0ec309e..b513fdf2 100644 --- a/harnesses/token-quote-coverage/cmd/monitor/main.go +++ b/harnesses/token-quote-coverage/cmd/monitor/main.go @@ -23,7 +23,6 @@ func main() { fmt.Printf("Region: %s\n", cfg.MonitorRegion) fmt.Printf("Mobula key set: %v\n", cfg.MobulaAPIKey != "") - fmt.Printf("OKX key set: %v\n", cfg.OKXDEXAPIKey != "") fmt.Println("Metrics on :2112/metrics, logs on :2112/logs (LOGS_TOKEN gated)") fmt.Println() diff --git a/harnesses/token-quote-coverage/cmd/monitor/okxdex.go b/harnesses/token-quote-coverage/cmd/monitor/okxdex.go deleted file mode 100644 index 96276c56..00000000 --- a/harnesses/token-quote-coverage/cmd/monitor/okxdex.go +++ /dev/null @@ -1,101 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" -) - -const ( - okxUSDCSolana = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" - okxUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" - okxUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" -) - -// OKXDEXProvider hits the OKX DEX aggregator quote API. -// OKX_DEX_API_KEY is optional — anonymous requests are allowed but may be rate-limited. -type OKXDEXProvider struct { - apiKey string - client *http.Client -} - -func NewOKXDEXProvider(apiKey string) *OKXDEXProvider { - return &OKXDEXProvider{apiKey: apiKey, client: newWarmHTTPClient()} -} - -func (p *OKXDEXProvider) Slug() string { return "okx-dex" } - -func (p *OKXDEXProvider) SupportsChain(chain string) bool { - switch chain { - case "solana", "base", "bsc": - return true - } - return false -} - -func (p *OKXDEXProvider) Quote(ctx context.Context, token Token) (ok bool) { - var chainId, usdcAddr string - switch token.Chain { - case "solana": - chainId = "501" - usdcAddr = okxUSDCSolana - case "base": - chainId = "8453" - usdcAddr = okxUSDCBase - case "bsc": - chainId = "56" - usdcAddr = okxUSDCBSC - default: - RecordProbe(p.Slug(), token.Venue, token.Chain, false) - return false - } - - q := url.Values{} - q.Set("chainId", chainId) - q.Set("fromTokenAddress", usdcAddr) - q.Set("toTokenAddress", token.Address) - q.Set("amount", "1000000") - endpoint := "https://www.okx.com/api/v5/dex/aggregator/quote?" + q.Encode() - - req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) - if err != nil { - RecordProbe(p.Slug(), token.Venue, token.Chain, false) - return false - } - if p.apiKey != "" { - req.Header.Set("OK-ACCESS-KEY", p.apiKey) - } - - resp, err := p.client.Do(req) - if err != nil { - fmt.Printf("[okx-dex] %s/%s net error: %s\n", token.Chain, token.Address, classifyNetErr(err)) - RecordProbe(p.Slug(), token.Venue, token.Chain, false) - return false - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - - if resp.StatusCode != 200 { - fmt.Printf("[okx-dex] %s/%s status=%d body=%s\n", token.Chain, token.Address, resp.StatusCode, snippet(body)) - RecordProbe(p.Slug(), token.Venue, token.Chain, false) - return false - } - - var r struct { - Data []struct { - ToTokenAmount string `json:"toTokenAmount"` - } `json:"data"` - } - if err := json.Unmarshal(body, &r); err != nil { - fmt.Printf("[okx-dex] %s/%s parse error: %v\n", token.Chain, token.Address, err) - RecordProbe(p.Slug(), token.Venue, token.Chain, false) - return false - } - - ok = len(r.Data) > 0 && r.Data[0].ToTokenAmount != "" && r.Data[0].ToTokenAmount != "0" - RecordProbe(p.Slug(), token.Venue, token.Chain, ok) - return ok -} diff --git a/harnesses/token-quote-coverage/cmd/monitor/oneinch.go b/harnesses/token-quote-coverage/cmd/monitor/oneinch.go deleted file mode 100644 index 879c74cc..00000000 --- a/harnesses/token-quote-coverage/cmd/monitor/oneinch.go +++ /dev/null @@ -1,99 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" -) - -const ( - oneinchUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" - oneinchUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" -) - -// OneInchProvider hits api.1inch.dev for Base and BSC. -type OneInchProvider struct { - apiKey string - client *http.Client -} - -func NewOneInchProvider(apiKey string) *OneInchProvider { - return &OneInchProvider{apiKey: apiKey, client: newWarmHTTPClient()} -} - -func (p *OneInchProvider) Slug() string { return "1inch" } - -func (p *OneInchProvider) SupportsChain(chain string) bool { - return chain == "base" || chain == "bsc" -} - -func (p *OneInchProvider) Quote(ctx context.Context, token Token) (ok bool) { - var chainId int - var usdcAddr string - switch token.Chain { - case "base": - chainId = 8453 - usdcAddr = oneinchUSDCBase - case "bsc": - chainId = 56 - usdcAddr = oneinchUSDCBSC - default: - RecordProbe(p.Slug(), token.Venue, token.Chain, false) - return false - } - - q := url.Values{} - q.Set("src", usdcAddr) - q.Set("dst", token.Address) - q.Set("amount", "1000000") - endpoint := fmt.Sprintf("https://api.1inch.dev/swap/v6.0/%d/quote?%s", chainId, q.Encode()) - - req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) - if err != nil { - RecordProbe(p.Slug(), token.Venue, token.Chain, false) - return false - } - req.Header.Set("Authorization", "Bearer "+p.apiKey) - req.Header.Set("Accept", "application/json") - - resp, err := p.client.Do(req) - if err != nil { - fmt.Printf("[1inch] %s/%s net error: %s\n", token.Chain, token.Address, classifyNetErr(err)) - RecordProbe(p.Slug(), token.Venue, token.Chain, false) - return false - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - - if resp.StatusCode == 429 { - fmt.Printf("[1inch] %s/%s throttled\n", token.Chain, token.Address) - RecordProbe(p.Slug(), token.Venue, token.Chain, false) - return false - } - if resp.StatusCode == 401 || resp.StatusCode == 403 { - fmt.Printf("[1inch] auth error %d\n", resp.StatusCode) - RecordProbe(p.Slug(), token.Venue, token.Chain, false) - return false - } - if resp.StatusCode != 200 { - fmt.Printf("[1inch] %s/%s status=%d body=%s\n", token.Chain, token.Address, resp.StatusCode, snippet(body)) - RecordProbe(p.Slug(), token.Venue, token.Chain, false) - return false - } - - var r struct { - ToAmount string `json:"toAmount"` - } - if err := json.Unmarshal(body, &r); err != nil { - fmt.Printf("[1inch] %s/%s parse error: %v\n", token.Chain, token.Address, err) - RecordProbe(p.Slug(), token.Venue, token.Chain, false) - return false - } - - ok = r.ToAmount != "" && r.ToAmount != "0" - RecordProbe(p.Slug(), token.Venue, token.Chain, ok) - return ok -} diff --git a/harnesses/token-quote-coverage/cmd/monitor/scheduler.go b/harnesses/token-quote-coverage/cmd/monitor/scheduler.go index 34b3573d..232a5dc5 100644 --- a/harnesses/token-quote-coverage/cmd/monitor/scheduler.go +++ b/harnesses/token-quote-coverage/cmd/monitor/scheduler.go @@ -19,7 +19,6 @@ func buildProviders(cfg *Config) []Provider { NewJupiterProvider(), NewMobulaProvider(cfg.MobulaAPIKey), NewKyberSwapProvider(), - NewOKXDEXProvider(cfg.OKXDEXAPIKey), NewOdosProvider(), } return providers diff --git a/src/lib/bench-template.ts b/src/lib/bench-template.ts index f1addc5f..834e9fc2 100644 --- a/src/lib/bench-template.ts +++ b/src/lib/bench-template.ts @@ -111,19 +111,19 @@ export function renderTemplate(text: string, benchmark: Benchmark): string { const k = keyword.toLowerCase(); if (k === "best_name") { const lead = bestForChain(benchmark, chain); - return lead ? lead.name : whole; + return lead ? lead.name : ""; } if (k === "best_p50") { const lead = bestForChain(benchmark, chain); - return lead ? fmtUnit(lead.ms.p50, benchmark.unit) : whole; + return lead ? fmtUnit(lead.ms.p50, benchmark.unit) : ""; } if (k === "worst_name") { const trailer = worstForChain(benchmark, chain); - return trailer ? trailer.name : whole; + return trailer ? trailer.name : ""; } if (k === "worst_p50") { const trailer = worstForChain(benchmark, chain); - return trailer ? fmtUnit(trailer.ms.p50, benchmark.unit) : whole; + return trailer ? fmtUnit(trailer.ms.p50, benchmark.unit) : ""; } return whole; }, @@ -146,13 +146,13 @@ export function renderTemplate(text: string, benchmark: Benchmark): string { return fmtUnit(raw, benchmark.unit); } case "best_name": - return best ? best.name : whole; + return best ? best.name : ""; case "best_p50": - return best ? fmtUnit(best.ms.p50, benchmark.unit) : whole; + return best ? fmtUnit(best.ms.p50, benchmark.unit) : ""; case "worst_name": - return worst ? worst.name : whole; + return worst ? worst.name : ""; case "worst_p50": - return worst ? fmtUnit(worst.ms.p50, benchmark.unit) : whole; + return worst ? fmtUnit(worst.ms.p50, benchmark.unit) : ""; case "count": return String(live.length); default: