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: 18 additions & 4 deletions benchmarks/network-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ seo_intro: |
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 five providers (GeckoTerminal,
Codex, Mobula, CoinPaprika and Dune via Sim API), deduplicates by
chain id and counts. Mainnet only,
official listing every six hours from six providers (GeckoTerminal,
Codex, Mobula, CoinPaprika, 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 @@ -47,6 +47,7 @@ methodology:
- "Codex: GraphQL `getNetworks` query at https://graph.codex.io/graphql with an official API key."
- "Mobula: GET /api/1/blockchains with an Authorization API key."
- "CoinPaprika: GET /v1/contracts (no auth). Lists platforms supported for contract lookup."
- "CoinStats: GET /wallet/blockchains with X-API-KEY."
- "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."
Expand Down Expand Up @@ -81,7 +82,7 @@ faq:

# Real metrics exposed by the network-coverage harness:
# networks_supported_total{provider="geckoterminal"|"codex"|"mobula"|
# "coinpaprika"|"dune"}
# "coinpaprika"|"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 @@ -140,6 +141,19 @@ providers:
sample_size: networks_supported_total{provider="coinpaprika"}
series: networks_supported_total{provider="coinpaprika"}

- slug: coinstats
name: CoinStats
tag: Portfolio + market data
formula: "Count of blockchains returned by CoinStats's `/wallet/blockchains` endpoint, refreshed every 6 hours."
queries:
p50: networks_supported_total{provider="coinstats"}
p90: networks_supported_total{provider="coinstats"}
p99: networks_supported_total{provider="coinstats"}
mean: networks_supported_total{provider="coinstats"}
success: clamp_max(networks_supported_total{provider="coinstats"} > bool 0, 1)
sample_size: networks_supported_total{provider="coinstats"}
series: networks_supported_total{provider="coinstats"}

- slug: dune
name: Dune
tag: Onchain analytics + Sim API
Expand Down
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
}
6 changes: 4 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,7 @@ 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
CoinStatsAPIKey string
SimDuneAPIKey string // optional — Sim's public endpoint works keyless, but a key avoids rate limits
HTTPProxy string
RefreshInterval time.Duration
Expand All @@ -24,6 +25,7 @@ func loadConfig() *Config {
CodexAPIKey: os.Getenv("CODEX_API_KEY"),
CodexSessionCookie: os.Getenv("DEFINED_SESSION_COOKIE"),
DefinedTokenURL: os.Getenv("DEFINED_TOKEN_SERVICE_URL"),
CoinStatsAPIKey: os.Getenv("COINSTATS_API_KEY"),
SimDuneAPIKey: os.Getenv("SIM_DUNE_API_KEY"),
HTTPProxy: os.Getenv("HTTP_PROXY"),
RefreshInterval: 6 * time.Hour,
Expand All @@ -45,8 +47,8 @@ func loadConfig() *Config {
} else if c.CodexSessionCookie != "" {
codexAuth = "cookie+mint"
}
fmt.Printf("Config: refresh=%v, testnets=%v, mobula_key=%v, codex=%s, sim_dune_key=%v\n",
fmt.Printf("Config: refresh=%v, testnets=%v, mobula_key=%v, codex=%s, coinstats_key=%v, sim_dune_key=%v\n",
c.RefreshInterval, c.IncludeTestnets, c.MobulaAPIKey != "", codexAuth,
c.SimDuneAPIKey != "")
c.CoinStatsAPIKey != "", c.SimDuneAPIKey != "")
return c
}
6 changes: 1 addition & 5 deletions harnesses/network-coverage/cmd/script/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,7 @@ func fetchAll(cfg *Config) {
{"codex", fetchCodex},
{"coinpaprika", fetchCoinPaprika},
{"dune", fetchSimDune},
// coinstats dropped 2026-07-16: the free tier exhausts credits
// well below the harness cadence, so /wallet/blockchains returns
// HTTP 406 "Credits limit reached" on every fetch and never
// publishes a networks count. Re-add when a paid key is
// provisioned; the removed adapter is one revert away in git.
{"coinstats", fetchCoinStats},
}

var wg sync.WaitGroup
Expand Down
Loading