diff --git a/benchmarks/network-coverage.yml b/benchmarks/network-coverage.yml index 3ae529d1..1da9e4a2 100644 --- a/benchmarks/network-coverage.yml +++ b/benchmarks/network-coverage.yml @@ -3,8 +3,8 @@ slug: network-coverage number: "005" title: Onchain data API with most networks supported -seo_title: "Crypto data API with the most blockchains supported in 2026: Mobula, Codex, GeckoTerminal, Covalent, CoinPaprika, CoinStats, Sim by Dune live" -seo_description: "Live blockchain coverage leaderboard. How many mainnets each onchain data API (Mobula, Codex, GeckoTerminal, Covalent/GoldRush, CoinPaprika, CoinStats, Sim by Dune) officially supports right now, audited every six hours against their public network endpoints." +seo_title: "Crypto data API with the most blockchains supported in 2026: Mobula, Codex, GeckoTerminal, Covalent, CoinPaprika, CoinStats, Dune live" +seo_description: "Live blockchain coverage leaderboard. How many mainnets each onchain data API (Mobula, Codex, GeckoTerminal, Covalent/GoldRush, CoinPaprika, CoinStats, Dune) officially supports right now, audited every six hours against their public network endpoints." subtitle: Number of blockchains each major onchain data provider officially supports. category: Aggregators status: live @@ -33,8 +33,8 @@ abstract: | We benchmark how many networks each major onchain data provider lists in its public "supported networks" endpoint. The harness fetches the official listing every six hours from seven providers (GeckoTerminal, - Codex, Mobula, CoinPaprika, Covalent/GoldRush, CoinStats and Sim by - Dune), deduplicates by chain id and counts. Mainnet only, + Codex, Mobula, CoinPaprika, Covalent/GoldRush, CoinStats and Dune via + Sim API), deduplicates by chain id and counts. Mainnet only, testnets are excluded because providers list them inconsistently and the comparison is meant to reflect what a builder can integrate against in production. Coverage breadth is one dimension of a data provider's @@ -49,7 +49,7 @@ methodology: - "CoinPaprika: GET /v1/contracts (no auth) — list of platforms supported for contract lookup." - "Covalent / GoldRush: GET /v1/chains/ with a Bearer API key; testnets filtered via `is_testnet`." - "CoinStats: GET /wallet/blockchains with X-API-KEY." - - "Sim by Dune: GET /v1/evm/supported-chains (no auth) — EVM only, mainnets filtered via the `mainnet` tag." + - "Dune (via Sim API): GET https://api.sim.dune.com/v1/evm/supported-chains (no auth) — EVM only, mainnets filtered via the `mainnet` tag." - "Cadence: full refresh every 6 hours." - "Counting: a provider's network is counted once per unique chain id; mainnet only." - "Failures (network errors, rate limits, auth errors) leave the previous count in place and increment a fetch_errors counter. the page falls back to its last successful sample." @@ -84,7 +84,7 @@ faq: # Real metrics exposed by the network-coverage harness: # networks_supported_total{provider="geckoterminal"|"codex"|"mobula"| # "coinpaprika"|"covalent"| -# "coinstats"|"sim-dune"} +# "coinstats"|"dune"} # -> gauge, the unique-chain count from the latest successful refresh. # network_supported{provider, chain_id, slug, name} -> gauge (1 per # network; useful for diff queries on the site if we add a @@ -169,15 +169,15 @@ providers: sample_size: networks_supported_total{provider="coinstats"} series: networks_supported_total{provider="coinstats"} - - slug: sim-dune - name: Sim by Dune - tag: EVM data API (Dune Sim) + - slug: dune + name: Dune + tag: Onchain analytics + Sim API formula: "Count of EVM chains tagged `mainnet` in Sim by Dune's `/v1/evm/supported-chains` endpoint, refreshed every 6 hours. EVM-only." queries: - p50: networks_supported_total{provider="sim-dune"} - p90: networks_supported_total{provider="sim-dune"} - p99: networks_supported_total{provider="sim-dune"} - mean: networks_supported_total{provider="sim-dune"} - success: clamp_max(networks_supported_total{provider="sim-dune"} > bool 0, 1) - sample_size: networks_supported_total{provider="sim-dune"} - series: networks_supported_total{provider="sim-dune"} + p50: networks_supported_total{provider="dune"} + p90: networks_supported_total{provider="dune"} + p99: networks_supported_total{provider="dune"} + mean: networks_supported_total{provider="dune"} + success: clamp_max(networks_supported_total{provider="dune"} > bool 0, 1) + sample_size: networks_supported_total{provider="dune"} + series: networks_supported_total{provider="dune"} diff --git a/harnesses/network-coverage/cmd/script/coinpaprika.go b/harnesses/network-coverage/cmd/script/coinpaprika.go new file mode 100644 index 00000000..46bd5696 --- /dev/null +++ b/harnesses/network-coverage/cmd/script/coinpaprika.go @@ -0,0 +1,56 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +const coinpaprikaContractsURL = "https://api.coinpaprika.com/v1/contracts" + +// CoinPaprika's /v1/contracts returns a flat array of platform slugs in +// "{symbol}-{name}" form (e.g. "eth-ethereum", "trx-tron"). It is the closest +// thing they expose to a list of "chains supported for contract lookup". +func fetchCoinPaprika(_ *Config) ProviderResult { + res := ProviderResult{Provider: "coinpaprika"} + + client := &http.Client{Timeout: 15 * time.Second} + req, _ := http.NewRequest("GET", coinpaprikaContractsURL, nil) + req.Header.Set("Accept", "application/json") + + resp, err := client.Do(req) + if err != nil { + res.Err = fmt.Sprintf("request_error: %v", err) + return res + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != 200 { + res.Err = fmt.Sprintf("status_%d", resp.StatusCode) + return res + } + + var parsed []string + if err := json.Unmarshal(body, &parsed); err != nil { + res.Err = fmt.Sprintf("parse_error: %v", err) + return res + } + + for _, slug := range parsed { + name := slug + if idx := strings.Index(slug, "-"); idx >= 0 && idx+1 < len(slug) { + name = strings.ReplaceAll(slug[idx+1:], "-", " ") + } + res.Networks = append(res.Networks, Network{ + ChainID: "", + Slug: slug, + Name: name, + }) + } + + return res +} diff --git a/harnesses/network-coverage/cmd/script/coinstats.go b/harnesses/network-coverage/cmd/script/coinstats.go new file mode 100644 index 00000000..344c6ee7 --- /dev/null +++ b/harnesses/network-coverage/cmd/script/coinstats.go @@ -0,0 +1,62 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +const coinstatsBlockchainsURL = "https://openapiv1.coinstats.app/wallet/blockchains" + +// CoinStats /wallet/blockchains returns a flat JSON array of: +// {connectionId, name, icon, chain} +// `connectionId` is the slug (e.g. "binancesmartchain"), `chain` is the +// category enum (e.g. "binance_smart"), `name` is the human label. +type coinstatsBlockchain struct { + ConnectionID string `json:"connectionId"` + Name string `json:"name"` + Chain string `json:"chain"` +} + +func fetchCoinStats(cfg *Config) ProviderResult { + res := ProviderResult{Provider: "coinstats"} + if cfg.CoinStatsAPIKey == "" { + res.Err = "missing_api_key" + return res + } + + client := &http.Client{Timeout: 15 * time.Second} + req, _ := http.NewRequest("GET", coinstatsBlockchainsURL, nil) + req.Header.Set("X-API-KEY", cfg.CoinStatsAPIKey) + req.Header.Set("Accept", "application/json") + + resp, err := client.Do(req) + if err != nil { + res.Err = fmt.Sprintf("request_error: %v", err) + return res + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != 200 { + res.Err = fmt.Sprintf("status_%d", resp.StatusCode) + return res + } + + var arr []coinstatsBlockchain + if err := json.Unmarshal(body, &arr); err != nil { + res.Err = fmt.Sprintf("parse_error: %v", err) + return res + } + + for _, b := range arr { + res.Networks = append(res.Networks, Network{ + ChainID: b.Chain, + Slug: b.ConnectionID, + Name: b.Name, + }) + } + return res +} diff --git a/harnesses/network-coverage/cmd/script/config.go b/harnesses/network-coverage/cmd/script/config.go index 0b356452..311ace2b 100644 --- a/harnesses/network-coverage/cmd/script/config.go +++ b/harnesses/network-coverage/cmd/script/config.go @@ -12,6 +12,9 @@ type Config struct { CodexAPIKey string // official Codex Bearer (preferred — no mint, no proxy) CodexSessionCookie string // fallback path: mint JWT from Defined.fi cookie DefinedTokenURL string // optional: pre-minted JWT sidecar + CovalentAPIKey string + CoinStatsAPIKey string + SimDuneAPIKey string // optional — Sim's public endpoint works keyless, but a key avoids rate limits HTTPProxy string RefreshInterval time.Duration IncludeTestnets bool @@ -23,6 +26,9 @@ func loadConfig() *Config { CodexAPIKey: os.Getenv("CODEX_API_KEY"), CodexSessionCookie: os.Getenv("DEFINED_SESSION_COOKIE"), DefinedTokenURL: os.Getenv("DEFINED_TOKEN_SERVICE_URL"), + CovalentAPIKey: os.Getenv("COVALENT_API_KEY"), + CoinStatsAPIKey: os.Getenv("COINSTATS_API_KEY"), + SimDuneAPIKey: os.Getenv("SIM_DUNE_API_KEY"), HTTPProxy: os.Getenv("HTTP_PROXY"), RefreshInterval: 6 * time.Hour, IncludeTestnets: false, @@ -43,7 +49,8 @@ func loadConfig() *Config { } else if c.CodexSessionCookie != "" { codexAuth = "cookie+mint" } - fmt.Printf("Config: refresh=%v, testnets=%v, mobula_key=%v, codex=%s\n", - c.RefreshInterval, c.IncludeTestnets, c.MobulaAPIKey != "", codexAuth) + fmt.Printf("Config: refresh=%v, testnets=%v, mobula_key=%v, codex=%s, covalent_key=%v, coinstats_key=%v, sim_dune_key=%v\n", + c.RefreshInterval, c.IncludeTestnets, c.MobulaAPIKey != "", codexAuth, + c.CovalentAPIKey != "", c.CoinStatsAPIKey != "", c.SimDuneAPIKey != "") return c } diff --git a/harnesses/network-coverage/cmd/script/covalent.go b/harnesses/network-coverage/cmd/script/covalent.go new file mode 100644 index 00000000..740d22b2 --- /dev/null +++ b/harnesses/network-coverage/cmd/script/covalent.go @@ -0,0 +1,83 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +const covalentChainsURL = "https://api.covalenthq.com/v1/chains/" + +// Covalent returns chain_id as a numeric string even for canonical EVM +// chains (e.g. "1" for Ethereum). Keep the raw string to avoid losing +// precision on non-EVM chains where the id can exceed int64 range. +type covalentChain struct { + ChainID string `json:"chain_id"` + Name string `json:"name"` + Label string `json:"label"` + IsTestnet bool `json:"is_testnet"` +} + +type covalentResponse struct { + Data struct { + Items []covalentChain `json:"items"` + } `json:"data"` + Error bool `json:"error"` + ErrorMessage string `json:"error_message"` +} + +func fetchCovalent(cfg *Config) ProviderResult { + res := ProviderResult{Provider: "covalent"} + if cfg.CovalentAPIKey == "" { + res.Err = "missing_api_key" + return res + } + + client := &http.Client{Timeout: 15 * time.Second} + req, _ := http.NewRequest("GET", covalentChainsURL, nil) + req.Header.Set("Authorization", "Bearer "+cfg.CovalentAPIKey) + req.Header.Set("Accept", "application/json") + + resp, err := client.Do(req) + if err != nil { + res.Err = fmt.Sprintf("request_error: %v", err) + return res + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != 200 { + res.Err = fmt.Sprintf("status_%d", resp.StatusCode) + return res + } + + var parsed covalentResponse + if err := json.Unmarshal(body, &parsed); err != nil { + res.Err = fmt.Sprintf("parse_error: %v", err) + return res + } + if parsed.Error { + res.Err = "api_error: " + parsed.ErrorMessage + return res + } + + for _, c := range parsed.Data.Items { + if c.IsTestnet && !cfg.IncludeTestnets { + continue + } + label := c.Label + if label == "" { + label = c.Name + } + res.Networks = append(res.Networks, Network{ + ChainID: c.ChainID, + Slug: c.Name, + Name: label, + Testnet: c.IsTestnet, + }) + } + + return res +} diff --git a/harnesses/network-coverage/cmd/script/main.go b/harnesses/network-coverage/cmd/script/main.go index 04c7f24f..bcaab8b0 100644 --- a/harnesses/network-coverage/cmd/script/main.go +++ b/harnesses/network-coverage/cmd/script/main.go @@ -71,6 +71,10 @@ func fetchAll(cfg *Config) { {"geckoterminal", fetchGeckoTerminal}, {"mobula", fetchMobula}, {"codex", fetchCodex}, + {"coinpaprika", fetchCoinPaprika}, + {"dune", fetchSimDune}, + {"covalent", fetchCovalent}, + {"coinstats", fetchCoinStats}, } var wg sync.WaitGroup diff --git a/harnesses/network-coverage/cmd/script/simdune.go b/harnesses/network-coverage/cmd/script/simdune.go new file mode 100644 index 00000000..508abf0d --- /dev/null +++ b/harnesses/network-coverage/cmd/script/simdune.go @@ -0,0 +1,76 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "time" +) + +const simDuneChainsURL = "https://api.sim.dune.com/v1/evm/supported-chains" + +type simDuneChain struct { + Name string `json:"name"` + ChainID int `json:"chain_id"` + Tags []string `json:"tags"` +} + +type simDuneResponse struct { + Chains []simDuneChain `json:"chains"` +} + +// fetchSimDune queries Sim by Dune's public supported-chains endpoint. +// EVM-only by design. Mainnets are tagged "mainnet"; everything else is +// treated as testnet/preview and filtered out unless IncludeTestnets is on. +func fetchSimDune(cfg *Config) ProviderResult { + res := ProviderResult{Provider: "dune"} + + client := &http.Client{Timeout: 15 * time.Second} + req, _ := http.NewRequest("GET", simDuneChainsURL, nil) + req.Header.Set("Accept", "application/json") + if cfg.SimDuneAPIKey != "" { + req.Header.Set("X-Sim-Api-Key", cfg.SimDuneAPIKey) + } + + resp, err := client.Do(req) + if err != nil { + res.Err = fmt.Sprintf("request_error: %v", err) + return res + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != 200 { + res.Err = fmt.Sprintf("status_%d", resp.StatusCode) + return res + } + + var parsed simDuneResponse + if err := json.Unmarshal(body, &parsed); err != nil { + res.Err = fmt.Sprintf("parse_error: %v", err) + return res + } + + for _, c := range parsed.Chains { + isMainnet := false + for _, t := range c.Tags { + if t == "mainnet" { + isMainnet = true + break + } + } + if !isMainnet && !cfg.IncludeTestnets { + continue + } + res.Networks = append(res.Networks, Network{ + ChainID: strconv.Itoa(c.ChainID), + Slug: c.Name, + Name: c.Name, + Testnet: !isMainnet, + }) + } + + return res +} diff --git a/src/data/provider-registry.ts b/src/data/provider-registry.ts index 8b798c82..63dbb8cd 100644 --- a/src/data/provider-registry.ts +++ b/src/data/provider-registry.ts @@ -76,13 +76,7 @@ export const PROVIDER_REGISTRY: Record = { dune: { url: "https://dune.com", description: - "Onchain analytics platform. SQL-queryable indexed data across EVM chains and Solana, public dashboards, plus the Sim onchain-data API for wallet, transaction, and token endpoints.", - twitter: "@DuneAnalytics", - }, - "sim-dune": { - url: "https://sim.dune.com", - description: - "Onchain-data REST API by Dune. EVM wallet balances, transactions, token info, holders, collectibles, and DeFi positions across 60+ mainnets.", + "Onchain analytics platform. SQL-queryable indexed data across EVM chains and Solana, public dashboards, plus the Sim REST API for wallet balances, transactions, token info, holders, and DeFi positions across 60+ EVM mainnets.", twitter: "@DuneAnalytics", }, coinpaprika: { diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index 21ad2408..73fee122 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -165,8 +165,6 @@ const ALIASES: Record = { "avalanche-official": "avalanche", "optimism-official": "optimism", - // Sim by Dune is Dune's onchain-data product — reuses Dune's brand mark. - "sim-dune": "dune", }; export function logoPath(slug: string): string | null {