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
32 changes: 16 additions & 16 deletions benchmarks/network-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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."
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"}
56 changes: 56 additions & 0 deletions harnesses/network-coverage/cmd/script/coinpaprika.go
Original file line number Diff line number Diff line change
@@ -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
}
62 changes: 62 additions & 0 deletions harnesses/network-coverage/cmd/script/coinstats.go
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 9 additions & 2 deletions harnesses/network-coverage/cmd/script/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
}
83 changes: 83 additions & 0 deletions harnesses/network-coverage/cmd/script/covalent.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions harnesses/network-coverage/cmd/script/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions harnesses/network-coverage/cmd/script/simdune.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading