diff --git a/benchmarks/token-quote-coverage.yml b/benchmarks/token-quote-coverage.yml index 6054257c..c5460c67 100644 --- a/benchmarks/token-quote-coverage.yml +++ b/benchmarks/token-quote-coverage.yml @@ -23,7 +23,7 @@ per_chain_explainer: - slug: robinhood h2: "DEX aggregator coverage on Robinhood Chain" body: | - Robinhood Chain (chainId 4663) is Robinhood's EVM-compatible Layer 2, launched in 2025. Tokens on Robinhood launch and graduate via the Pons launchpad to Uniswap V3-based pools, with USDG as the primary stablecoin. The bench probes all EVM aggregators (KyberSwap, Odos, Mobula) against Pons-graduated tokens. As of launch, no major third-party aggregator has integrated Robinhood chain routing, so coverage is expected to be near 0% across all providers — this gap is the data point. The bench will capture the first aggregator to integrate Robinhood and reflect coverage improvements in real time. + Robinhood Chain (chainId 4663) is Robinhood's EVM-compatible Layer 2, launched in 2025. Tokens on Robinhood launch and graduate via the Pons launchpad to Uniswap V3-based pools, with USDG as the primary stablecoin. The bench probes all EVM aggregators (KyberSwap, Mobula) against Pons-graduated tokens. As of launch, no major third-party aggregator has integrated Robinhood chain routing, so coverage is expected to be near 0% across all providers — this gap is the data point. The bench will capture the first aggregator to integrate Robinhood and reflect coverage improvements in real time. category: Aggregators status: draft @@ -74,9 +74,9 @@ abstract: | methodology: - "Token discovery: three sources run in parallel each hour. (1) Dexscreener /token-boosts/latest/v1 covers Solana (pump-fun, moonshot, meteora-dbc) and Robinhood chain (pons). (2) Virtuals Protocol /api/virtuals?filters[status]=SENTIENT covers Base graduated agents (virtuals venue). (3) GeckoTerminal /api/v2/networks/bsc/new_pools covers BSC (four-meme). All token addresses are enriched via a Dexscreener /latest/dex/tokens batch call to resolve the originating dexId and map 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: 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." + - "Aggregator endpoints: Mobula /api/2/swap/quoting (Bearer auth, EVM), Jupiter lite-api.jup.ag/swap/v1/quote (Solana), Raydium api-v3.raydium.io/compute/swap-base-in (Solana), KyberSwap aggregator-api.kyberswap.com/{chain}/api/v1/routes (EVM), ParaSwap apiv5.paraswap.io/prices (EVM), OpenOcean open-api.openocean.finance/v3/{chain}/swap_quote (EVM+Solana). 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, Odos, and Mobula probe EVM chains (Base, BNB, Robinhood). On Robinhood chain, all three currently return no-route errors recording 0% coverage — an accurate reflection that no major aggregator has integrated Robinhood yet. Providers are never penalised for chains they structurally do not support: Solana cells for EVM providers are null, not zero." + - "Chain-scope enforcement: Jupiter and Raydium probe Solana-only. KyberSwap and Mobula probe EVM chains (Base, BNB, Robinhood). ParaSwap probes Base and BNB. OpenOcean probes Base, BNB, and Solana. Robinhood chain is currently at 0% coverage across all providers — an accurate reflection that no major aggregator has integrated Robinhood yet. Providers are never penalised for chains they structurally do not support: Solana cells for EVM-only providers are null, not zero." - "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." @@ -180,3 +180,42 @@ 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: raydium + name: Raydium + tag: Solana only + formula: "Rolling 24h ratio for provider=raydium. Solana launchpads only (Pump.fun, Moonshot, Meteora DBC). EVM cells null." + queries: + p50: sum(increase(token_quote_coverage_success_total{provider="raydium"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="raydium"}[24h])) * 100 + p90: sum(increase(token_quote_coverage_attempts_total{provider="raydium"}[24h])) + p99: sum(increase(token_quote_coverage_success_total{provider="raydium"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="raydium"}[24h])) * 100 + mean: sum(increase(token_quote_coverage_success_total{provider="raydium"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="raydium"}[24h])) * 100 + success: clamp_max(sum(increase(token_quote_coverage_success_total{provider="raydium"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="raydium"}[24h])), 1) + sample_size: sum(increase(token_quote_coverage_attempts_total{provider="raydium"}[24h])) + series: sum(rate(token_quote_coverage_success_total{provider="raydium"}[1h])) / sum(rate(token_quote_coverage_attempts_total{provider="raydium"}[1h])) * 100 + + - 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." + 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])) + p99: sum(increase(token_quote_coverage_success_total{provider="paraswap"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="paraswap"}[24h])) * 100 + mean: sum(increase(token_quote_coverage_success_total{provider="paraswap"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="paraswap"}[24h])) * 100 + success: clamp_max(sum(increase(token_quote_coverage_success_total{provider="paraswap"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="paraswap"}[24h])), 1) + sample_size: sum(increase(token_quote_coverage_attempts_total{provider="paraswap"}[24h])) + series: sum(rate(token_quote_coverage_success_total{provider="paraswap"}[1h])) / sum(rate(token_quote_coverage_attempts_total{provider="paraswap"}[1h])) * 100 + + - slug: openocean + name: OpenOcean + tag: Multi-chain, Base + BNB + Solana + formula: "Rolling 24h ratio for provider=openocean. Covers Base, BNB, and Solana launchpads. Robinhood cells null." + queries: + p50: sum(increase(token_quote_coverage_success_total{provider="openocean"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="openocean"}[24h])) * 100 + p90: sum(increase(token_quote_coverage_attempts_total{provider="openocean"}[24h])) + p99: sum(increase(token_quote_coverage_success_total{provider="openocean"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="openocean"}[24h])) * 100 + mean: sum(increase(token_quote_coverage_success_total{provider="openocean"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="openocean"}[24h])) * 100 + success: clamp_max(sum(increase(token_quote_coverage_success_total{provider="openocean"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="openocean"}[24h])), 1) + sample_size: sum(increase(token_quote_coverage_attempts_total{provider="openocean"}[24h])) + series: sum(rate(token_quote_coverage_success_total{provider="openocean"}[1h])) / sum(rate(token_quote_coverage_attempts_total{provider="openocean"}[1h])) * 100 + diff --git a/harnesses/token-quote-coverage/cmd/monitor/openocean.go b/harnesses/token-quote-coverage/cmd/monitor/openocean.go new file mode 100644 index 00000000..51bd82c8 --- /dev/null +++ b/harnesses/token-quote-coverage/cmd/monitor/openocean.go @@ -0,0 +1,93 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const ( + openoceanUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" + openoceanUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" + openoceanUSDCSolana = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" +) + +type OpenOceanProvider struct { + client *http.Client +} + +func NewOpenOceanProvider() *OpenOceanProvider { + return &OpenOceanProvider{client: newWarmHTTPClient()} +} + +func (p *OpenOceanProvider) Slug() string { return "openocean" } + +func (p *OpenOceanProvider) SupportsChain(chain string) bool { + return chain == "base" || chain == "bsc" || chain == "solana" +} + +func (p *OpenOceanProvider) Quote(ctx context.Context, token Token) (ok bool) { + var chainSlug, usdcAddr string + switch token.Chain { + case "base": + chainSlug = "base" + usdcAddr = openoceanUSDCBase + case "bsc": + chainSlug = "bsc" + usdcAddr = openoceanUSDCBSC + case "solana": + chainSlug = "sol" + usdcAddr = openoceanUSDCSolana + default: + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + + q := url.Values{} + q.Set("inTokenAddress", usdcAddr) + q.Set("outTokenAddress", token.Address) + q.Set("amount", "1") + q.Set("gasPrice", "5") + q.Set("slippage", "50") + endpoint := fmt.Sprintf("https://open-api.openocean.finance/v3/%s/swap_quote?%s", chainSlug, q.Encode()) + + req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) + if err != nil { + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + + resp, err := p.client.Do(req) + if err != nil { + fmt.Printf("[openocean] %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("[openocean] %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 { + Code int `json:"code"` + Data struct { + OutAmount string `json:"outAmount"` + } `json:"data"` + } + if err := json.Unmarshal(body, &r); err != nil { + fmt.Printf("[openocean] %s/%s parse error: %v\n", token.Chain, token.Address, err) + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + + ok = r.Code == 200 && r.Data.OutAmount != "" && r.Data.OutAmount != "0" + RecordProbe(p.Slug(), token.Venue, token.Chain, ok) + return ok +} diff --git a/harnesses/token-quote-coverage/cmd/monitor/paraswap.go b/harnesses/token-quote-coverage/cmd/monitor/paraswap.go new file mode 100644 index 00000000..94e31ca7 --- /dev/null +++ b/harnesses/token-quote-coverage/cmd/monitor/paraswap.go @@ -0,0 +1,90 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const ( + paraswapUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" + paraswapUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" +) + +type ParaSwapProvider struct { + client *http.Client +} + +func NewParaSwapProvider() *ParaSwapProvider { + return &ParaSwapProvider{client: newWarmHTTPClient()} +} + +func (p *ParaSwapProvider) Slug() string { return "paraswap" } + +func (p *ParaSwapProvider) SupportsChain(chain string) bool { + return chain == "base" || chain == "bsc" +} + +func (p *ParaSwapProvider) Quote(ctx context.Context, token Token) (ok bool) { + var network, usdcAddr string + switch token.Chain { + case "base": + network = "8453" + usdcAddr = paraswapUSDCBase + case "bsc": + network = "56" + usdcAddr = paraswapUSDCBSC + default: + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + + q := url.Values{} + q.Set("srcToken", usdcAddr) + q.Set("destToken", token.Address) + q.Set("amount", "1000000") + q.Set("network", network) + q.Set("srcDecimals", "6") + q.Set("destDecimals", "18") + q.Set("side", "SELL") + endpoint := "https://apiv5.paraswap.io/prices?" + q.Encode() + + req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) + if err != nil { + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + + resp, err := p.client.Do(req) + if err != nil { + fmt.Printf("[paraswap] %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("[paraswap] %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 { + PriceRoute struct { + DestAmount string `json:"destAmount"` + } `json:"priceRoute"` + } + if err := json.Unmarshal(body, &r); err != nil { + fmt.Printf("[paraswap] %s/%s parse error: %v\n", token.Chain, token.Address, err) + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + + ok = r.PriceRoute.DestAmount != "" && r.PriceRoute.DestAmount != "0" + RecordProbe(p.Slug(), token.Venue, token.Chain, ok) + return ok +} diff --git a/harnesses/token-quote-coverage/cmd/monitor/raydium.go b/harnesses/token-quote-coverage/cmd/monitor/raydium.go new file mode 100644 index 00000000..e43ede5e --- /dev/null +++ b/harnesses/token-quote-coverage/cmd/monitor/raydium.go @@ -0,0 +1,78 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const raydiumUSDCSolana = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + +type RaydiumProvider struct { + client *http.Client +} + +func NewRaydiumProvider() *RaydiumProvider { + return &RaydiumProvider{client: newWarmHTTPClient()} +} + +func (p *RaydiumProvider) Slug() string { return "raydium" } + +func (p *RaydiumProvider) SupportsChain(chain string) bool { + return chain == "solana" +} + +func (p *RaydiumProvider) Quote(ctx context.Context, token Token) (ok bool) { + if token.Chain != "solana" { + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + + q := url.Values{} + q.Set("inputMint", raydiumUSDCSolana) + q.Set("outputMint", token.Address) + q.Set("amount", "1000000") + q.Set("slippageBps", "500") + q.Set("txVersion", "V0") + endpoint := "https://api-v3.raydium.io/compute/swap-base-in?" + q.Encode() + + req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) + if err != nil { + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + + resp, err := p.client.Do(req) + if err != nil { + fmt.Printf("[raydium] %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("[raydium] %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 { + Success bool `json:"success"` + Data struct { + OutputAmount string `json:"outputAmount"` + } `json:"data"` + } + if err := json.Unmarshal(body, &r); err != nil { + fmt.Printf("[raydium] %s/%s parse error: %v\n", token.Chain, token.Address, err) + RecordProbe(p.Slug(), token.Venue, token.Chain, false) + return false + } + + ok = r.Success && r.Data.OutputAmount != "" && r.Data.OutputAmount != "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 d9538cf1..46494003 100644 --- a/harnesses/token-quote-coverage/cmd/monitor/scheduler.go +++ b/harnesses/token-quote-coverage/cmd/monitor/scheduler.go @@ -17,8 +17,11 @@ const ( func buildProviders(cfg *Config) []Provider { providers := []Provider{ NewJupiterProvider(), + NewRaydiumProvider(), NewMobulaProvider(cfg.MobulaAPIKey), NewKyberSwapProvider(), + NewParaSwapProvider(), + NewOpenOceanProvider(), } return providers }