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
22 changes: 14 additions & 8 deletions benchmarks/token-quote-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ per_chain_explainer:
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; KyberSwap has structural coverage advantages over Solana-first providers here due to its live BNB pool-creation event index.
- 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.

category: Aggregators
status: draft
Expand Down Expand Up @@ -68,11 +72,11 @@ abstract: |
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."
- "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."
- "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 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."
- "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."
- "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 @@ -114,15 +118,17 @@ dimensions:
- { value: pump-fun, label: Pump.fun }
- { value: virtuals, label: Virtuals }
- { value: four-meme, label: Four.meme }
- { value: pons, label: Pons }
- { value: wow, label: WOW.XYZ }
- { value: moonshot, label: Moonshot }
- { value: meteora-dbc, label: Meteora DBC }
- { value: clanker, label: Clanker }
chain:
- { value: all, label: All chains }
- { value: solana, label: Solana }
- { value: base, label: Base }
- { value: bnb, label: BNB }
- { value: all, label: All chains }
- { value: solana, label: Solana }
- { value: base, label: Base }
- { value: bnb, label: BNB }
- { value: robinhood, label: Robinhood }

ledger_columns:
- { label: "Coverage rate", slot: p50, unit: pct }
Expand All @@ -137,7 +143,7 @@ rank_matrix_query: >
providers:
- slug: mobula
name: Mobula
tag: EVM only, Base + BNB, API key required
tag: EVM only, Base + BNB + Robinhood, API key required
formula: "Rolling 24h ratio of token_quote_coverage_success_total to token_quote_coverage_attempts_total for provider=mobula, across all launchpads and chains Mobula supports."
queries:
p50: sum(increase(token_quote_coverage_success_total{provider="mobula"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="mobula"}[24h])) * 100
Expand All @@ -163,7 +169,7 @@ providers:

- slug: kyberswap
name: KyberSwap
tag: EVM multi-chain, per-chain aggregator engine
tag: EVM multi-chain, Base + BNB + Robinhood
formula: "Rolling 24h ratio for provider=kyberswap. EVM launchpads only (Virtuals, WOW, Four.meme, Clanker). Solana cells null."
queries:
p50: sum(increase(token_quote_coverage_success_total{provider="kyberswap"}[24h])) / sum(increase(token_quote_coverage_attempts_total{provider="kyberswap"}[24h])) * 100
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func FetchBoostedTokens(ctx context.Context) ([]boostEntry, error) {
return nil, fmt.Errorf("dexscreener boosts parse: %w", err)
}

supported := map[string]bool{"solana": true, "base": true, "bsc": true}
supported := map[string]bool{"solana": true, "base": true, "bsc": true, "robinhood": true}
var out []boostEntry
seen := map[string]bool{}
for _, e := range entries {
Expand Down
68 changes: 68 additions & 0 deletions harnesses/token-quote-coverage/cmd/monitor/gecko.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package main

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)

// FetchNewBSCTokens returns base tokens from recently created pools on BSC
// via GeckoTerminal. Used as fallback since Four.meme API is not public.
func FetchNewBSCTokens(ctx context.Context) ([]boostEntry, error) {
req, err := http.NewRequestWithContext(ctx, "GET",
"https://api.geckoterminal.com/api/v2/networks/bsc/new_pools?page=1",
nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")

resp, err := dexClient.Do(req)
if err != nil {
return nil, fmt.Errorf("geckoterminal bsc: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)

if resp.StatusCode != 200 {
return nil, fmt.Errorf("geckoterminal bsc: status=%d", resp.StatusCode)
}

var result struct {
Data []struct {
Relationships struct {
BaseToken struct {
Data struct {
ID string `json:"id"` // "bsc_0x..."
} `json:"data"`
} `json:"base_token"`
} `json:"relationships"`
} `json:"data"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("geckoterminal bsc parse: %w", err)
}

var out []boostEntry
seen := map[string]bool{}
for _, pool := range result.Data {
id := pool.Relationships.BaseToken.Data.ID
if !strings.HasPrefix(id, "bsc_") {
continue
}
addr := id[4:]
k := strings.ToLower(addr)
if seen[k] {
continue
}
seen[k] = true
out = append(out, boostEntry{ChainId: "bsc", TokenAddress: addr})
if len(out) >= 20 {
break
}
}
return out, nil
}
10 changes: 7 additions & 3 deletions harnesses/token-quote-coverage/cmd/monitor/kyberswap.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (
)

const (
kyberUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
kyberUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"
kyberUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
kyberUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"
kyberUSDGRobinhood = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
)

// KyberSwapProvider hits aggregator-api.kyberswap.com for Base and BSC.
Expand All @@ -26,7 +27,7 @@ func NewKyberSwapProvider() *KyberSwapProvider {
func (p *KyberSwapProvider) Slug() string { return "kyberswap" }

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

func (p *KyberSwapProvider) Quote(ctx context.Context, token Token) (ok bool) {
Expand All @@ -38,6 +39,9 @@ func (p *KyberSwapProvider) Quote(ctx context.Context, token Token) (ok bool) {
case "bsc":
slug = "bsc"
usdcAddr = kyberUSDCBSC
case "robinhood":
slug = "robinhood"
usdcAddr = kyberUSDGRobinhood
default:
RecordProbe(p.Slug(), token.Venue, token.Chain, false)
return false
Expand Down
9 changes: 6 additions & 3 deletions harnesses/token-quote-coverage/cmd/monitor/mobula.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (
)

const (
mobulaUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
mobulaUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"
mobulaUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
mobulaUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"
mobulaUSDGRobinhood = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
// dummy wallet required by the quoting endpoint (no funds needed)
mobulaWallet = "0x0000000000000000000000000000000000000001"
)
Expand All @@ -31,7 +32,7 @@ func (p *MobulaProvider) Slug() string { return "mobula" }

func (p *MobulaProvider) SupportsChain(chain string) bool {
switch chain {
case "base", "bsc":
case "base", "bsc", "robinhood":
return true
}
return false
Expand All @@ -44,6 +45,8 @@ func (p *MobulaProvider) Quote(ctx context.Context, token Token) (ok bool) {
chainId, tokenIn = "8453", mobulaUSDCBase
case "bsc":
chainId, tokenIn = "56", mobulaUSDCBSC
case "robinhood":
chainId, tokenIn = "4663", mobulaUSDGRobinhood
default:
RecordProbe(p.Slug(), token.Venue, token.Chain, false)
return false
Expand Down
10 changes: 7 additions & 3 deletions harnesses/token-quote-coverage/cmd/monitor/odos.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (
)

const (
odosUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
odosUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"
odosUSDCBase = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
odosUSDCBSC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"
odosUSDGRobinhood = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"
)

// OdosProvider hits api.odos.xyz/sor/quote/v2 for Base and BSC.
Expand All @@ -26,7 +27,7 @@ func NewOdosProvider() *OdosProvider {
func (p *OdosProvider) Slug() string { return "odos" }

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

func (p *OdosProvider) Quote(ctx context.Context, token Token) (ok bool) {
Expand All @@ -39,6 +40,9 @@ func (p *OdosProvider) Quote(ctx context.Context, token Token) (ok bool) {
case "bsc":
chainId = 56
usdcAddr = odosUSDCBSC
case "robinhood":
chainId = 4663
usdcAddr = odosUSDGRobinhood
default:
RecordProbe(p.Slug(), token.Venue, token.Chain, false)
return false
Expand Down
6 changes: 4 additions & 2 deletions harnesses/token-quote-coverage/cmd/monitor/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import "context"
// Token is one recently-active token fetched from Dexscreener.
type Token struct {
Address string
Chain string // "solana", "base", "bsc"
Venue string // "pump-fun", "virtuals", "four-meme", etc.
Chain string // "solana", "base", "bsc", "robinhood"
Venue string // "pump-fun", "virtuals", "four-meme", "pons", etc.
}

// Provider is the chain-aware quote coverage adapter contract.
Expand Down Expand Up @@ -45,6 +45,8 @@ func venueFromPair(chainId, dexId string) string {
}
case "bsc":
return "four-meme"
case "robinhood":
return "pons"
}
return "all"
}
49 changes: 41 additions & 8 deletions harnesses/token-quote-coverage/cmd/monitor/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,55 @@ func runScheduler(cfg *Config, stopChan <-chan struct{}) {
}
}

// doTick fetches tokens from Dexscreener and probes all providers in parallel.
// doTick fetches tokens from all discovery sources in parallel, merges, and probes.
func doTick(ctx context.Context, providers []Provider) {
fetchCtx, cancel := context.WithTimeout(ctx, fetchTimeout)
defer cancel()

fmt.Println("[SCHED] fetching boosted tokens from Dexscreener")
entries, err := FetchBoostedTokens(fetchCtx)
if err != nil {
fmt.Printf("[SCHED] FetchBoostedTokens error: %v\n", err)
return
// Fetch from all discovery sources concurrently: Dexscreener boosts (Solana +
// Robinhood), Virtuals Protocol API (Base), GeckoTerminal new pools (BSC).
type result struct {
name string
entries []boostEntry
err error
}
ch := make(chan result, 3)
go func() {
e, err := FetchBoostedTokens(fetchCtx)
ch <- result{"dexscreener-boosts", e, err}
}()
go func() {
e, err := FetchVirtualsTokens(fetchCtx)
ch <- result{"virtuals", e, err}
}()
go func() {
e, err := FetchNewBSCTokens(fetchCtx)
ch <- result{"geckoterminal-bsc", e, err}
}()

seen := map[string]bool{}
var entries []boostEntry
for i := 0; i < 3; i++ {
r := <-ch
if r.err != nil {
fmt.Printf("[SCHED] %s error: %v\n", r.name, r.err)
continue
}
for _, e := range r.entries {
key := e.ChainId + ":" + e.TokenAddress
if !seen[key] {
seen[key] = true
entries = append(entries, e)
}
}
fmt.Printf("[SCHED] %s: %d tokens\n", r.name, len(r.entries))
}

if len(entries) == 0 {
fmt.Println("[SCHED] warning: Dexscreener returned 0 tokens")
fmt.Println("[SCHED] warning: all discovery sources returned 0 tokens")
return
}
fmt.Printf("[SCHED] got %d boosted tokens, enriching with venue data\n", len(entries))
fmt.Printf("[SCHED] %d unique tokens across all chains, enriching with venue data\n", len(entries))

tokens, err := EnrichWithVenue(fetchCtx, entries)
if err != nil {
Expand Down
73 changes: 73 additions & 0 deletions harnesses/token-quote-coverage/cmd/monitor/virtuals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package main

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)

// FetchVirtualsTokens returns recently graduated Virtuals Protocol agents on Base.
// SENTIENT status = agents that have a live DEX pool (real tokenAddress set).
// Sorted by lpCreatedAt desc so freshest graduates come first.
func FetchVirtualsTokens(ctx context.Context) ([]boostEntry, error) {
req, err := http.NewRequestWithContext(ctx, "GET",
"https://api.virtuals.io/api/virtuals?filters[status]=SENTIENT&sort[0]=lpCreatedAt:desc&pagination[pageSize]=20",
nil)
if err != nil {
return nil, err
}

resp, err := dexClient.Do(req)
if err != nil {
return nil, fmt.Errorf("virtuals: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)

if resp.StatusCode != 200 {
return nil, fmt.Errorf("virtuals: status=%d body=%s", resp.StatusCode, snippet(body))
}

// The API returns either {"data": [items]} or {"data": {"data": [items]}}.
// Handle both shapes.
var envelope struct {
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal(body, &envelope); err != nil {
return nil, fmt.Errorf("virtuals parse: %w", err)
}

type item struct {
TokenAddress string `json:"tokenAddress"`
}

var items []item
if err := json.Unmarshal(envelope.Data, &items); err != nil {
// try nested {"data": [...]}
var nested struct {
Data []item `json:"data"`
}
if err2 := json.Unmarshal(envelope.Data, &nested); err2 != nil {
return nil, fmt.Errorf("virtuals parse nested: %w", err2)
}
items = nested.Data
}

var out []boostEntry
seen := map[string]bool{}
for _, it := range items {
if it.TokenAddress == "" {
continue
}
k := strings.ToLower(it.TokenAddress)
if seen[k] {
continue
}
seen[k] = true
out = append(out, boostEntry{ChainId: "base", TokenAddress: it.TokenAddress})
}
return out, nil
}
Loading