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
45 changes: 42 additions & 3 deletions benchmarks/token-quote-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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

93 changes: 93 additions & 0 deletions harnesses/token-quote-coverage/cmd/monitor/openocean.go
Original file line number Diff line number Diff line change
@@ -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
}
90 changes: 90 additions & 0 deletions harnesses/token-quote-coverage/cmd/monitor/paraswap.go
Original file line number Diff line number Diff line change
@@ -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
}
78 changes: 78 additions & 0 deletions harnesses/token-quote-coverage/cmd/monitor/raydium.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions harnesses/token-quote-coverage/cmd/monitor/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ const (
func buildProviders(cfg *Config) []Provider {
providers := []Provider{
NewJupiterProvider(),
NewRaydiumProvider(),
NewMobulaProvider(cfg.MobulaAPIKey),
NewKyberSwapProvider(),
NewParaSwapProvider(),
NewOpenOceanProvider(),
}
return providers
}
Expand Down
Loading