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
12 changes: 7 additions & 5 deletions harnesses/chain-kpis/cmd/script/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,23 @@ import (
type Config struct {
// Mobula REST key. Required for /market/data and /market/blockchain/stats
// (both endpoints rate-limit free traffic to ~10 req/min, the key lifts
// that to a level that comfortably absorbs 21 chains × 2 endpoints
// that to a level that comfortably absorbs 32 chains × 2 endpoints
// every refresh tick.) Without a key, Mobula returns 429 immediately.
MobulaAPIKey string

// How often DefiLlama is polled per chain. 15 min is the conservative
// default: their public API is generous but the TVL value barely moves
// inside a 15-min window (intra-day deltas are noise vs the day-over-
// day signal we surface on the chain page). 3 calls/h × 21 chains = 63
// req/h, well under any sane rate-limit ceiling.
// day signal we surface on the chain page). 4 ticks/h × 31 mapped
// chains × 3 endpoints = 372 req/h, well under any sane rate-limit
// ceiling.
DefillamaRefreshInterval time.Duration

// Mobula tick. Faster because we own the source — and native-token
// prices move on shorter cycles than DEX-TVL aggregates. 5 min keeps
// the price card fresh without hammering. 12 calls/h × 21 chains × 2
// endpoints = 504 req/h, fine on a paid key.
// the price card fresh without hammering. Native calls dedup by
// symbol, so one tick is ~19 native plus ~27 stats requests, under
// 600 req/h, fine on a paid key.
MobulaRefreshInterval time.Duration
}

Expand Down
23 changes: 12 additions & 11 deletions harnesses/chain-kpis/cmd/script/defillama.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import (
// per chain that has a DefiLlama mapping:
// 1. /v2/historicalChainTvl/<name> — last sample = current TVL
// 2. /overview/dexs/<name> — total24h = aggregate DEX volume
// 3. /stablecoincharts/<name>?stablecoin=1
// — last point's USD-pegged total
// 3. /stablecoincharts/<name> — last point's USD-pegged total
//
// Each endpoint is its own goroutine inside fetchDefillamaChain so a
// stuck TVL request doesn't starve the stables fetch for the same chain.
Expand Down Expand Up @@ -113,20 +112,22 @@ func defillamaDexVolume24h(chainName string) (float64, error) {
return resp.Total24h, nil
}

// defillamaStablesMcap reads /stablecoincharts/<chain>?stablecoin=1 and
// returns the last sample's USD-pegged total. The `stablecoin=1` filter
// is required by the API though we don't filter by a specific stablecoin;
// the response is the chain-wide aggregate when no specific id is given.
// defillamaStablesMcap reads /stablecoincharts/<chain> and returns the
// last sample's USD-pegged total. No `stablecoin=<id>` query param: that
// param filters to ONE pegged asset (id 1 = USDT) and the API answers
// 200 + empty body on chains where that specific asset has no recorded
// issuance (Base, Blast, Stellar), which is what nulled their stables
// card. The unfiltered call returns the chain-wide aggregate.
// Chains DefiLlama tracks for TVL but not for stablecoins (Litecoin)
// answer 404 and land in the not_found error bucket, gauge unpublished.
func defillamaStablesMcap(chainName string) (float64, error) {
url := fmt.Sprintf("%s/stablecoincharts/%s?stablecoin=1", stablesLlamaBase, encodePath(chainName))
url := fmt.Sprintf("%s/stablecoincharts/%s", stablesLlamaBase, encodePath(chainName))
body, err := getJSON(httpClientDefillama, url)
if err != nil {
return 0, err
}
// DefiLlama returns 200 + empty body for chains it tracks but where no
// stablecoin issuance has been recorded (Stellar, Blast, Base at time
// of writing). Surface as a typed sentinel so the harness logs it as
// "not_tracked" rather than spamming parse_error.
// Typed sentinel so an empty 200 logs as "not_tracked" rather than
// spamming parse_error.
if len(body) == 0 {
return 0, fmt.Errorf("not_tracked")
}
Expand Down
4 changes: 2 additions & 2 deletions harnesses/chain-kpis/cmd/script/mobula.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ func fetchAllMobula(cfg *Config) {
return
}

// Dedup native symbols. The 21 chains today flatten to 11 unique
// natives (every L2 = ETH).
// Dedup native symbols. The 32 chains today flatten to ~19 unique
// natives (every ETH gas rollup = ETH).
natives := map[string][]Chain{}
for _, c := range Registry {
if c.NativeSymbol == "" {
Expand Down
32 changes: 31 additions & 1 deletion harnesses/chain-kpis/cmd/script/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ var Registry = []Chain{
{Slug: "bnb", DefiLlama: "BSC", Mobula: "BNB Smart Chain (BEP20)", NativeSymbol: "BNB"},
{Slug: "avalanche", DefiLlama: "Avalanche", Mobula: "Avalanche C-Chain", NativeSymbol: "AVAX"},
{Slug: "sui", DefiLlama: "Sui", Mobula: "Sui", NativeSymbol: "SUI"},
{Slug: "ton", DefiLlama: "TON", Mobula: "TON", NativeSymbol: "TON"},
// Site canonical slug is "gram" since the Toncoin to Gram rename; the
// old "ton" label made the Prom selector miss and the KV blob stay null.
// Mobula still serves the asset under symbol TON: symbol GRAM resolves
// to an unrelated "GRAM Token" (~$24M mcap), verified live 2026-07-08.
// The stats endpoint still accepts blockchain=TON even though it is
// absent from /api/1/blockchains.
{Slug: "gram", DefiLlama: "TON", Mobula: "TON", NativeSymbol: "TON"},
{Slug: "stellar", DefiLlama: "Stellar", Mobula: "", NativeSymbol: "XLM"},
{Slug: "tron", DefiLlama: "Tron", Mobula: "TRON", NativeSymbol: "TRX"},
{Slug: "cardano", DefiLlama: "Cardano", Mobula: "", NativeSymbol: "ADA"},
Expand All @@ -52,4 +58,28 @@ var Registry = []Chain{
{Slug: "blast", DefiLlama: "Blast", Mobula: "Blast", NativeSymbol: "ETH"},
{Slug: "mantle", DefiLlama: "Mantle", Mobula: "Mantle", NativeSymbol: "MNT"},
{Slug: "taiko", DefiLlama: "Taiko", Mobula: "Taiko", NativeSymbol: "ETH"},
// Chains added to the site registry after the original harness config
// was written; they published all-null KV blobs until this batch.
// Every DefiLlama name below verified live against /v2/chains,
// /overview/dexs/<name> and /stablecoincharts/<name> on 2026-07-08.
// Mobula blockchain names verified against /api/1/blockchains; empty
// means Mobula does not index the chain yet (only the tokens-indexed
// gauge is lost, the KPI strip does not read it).
{Slug: "monad", DefiLlama: "Monad", Mobula: "", NativeSymbol: "MON"},
{Slug: "megaeth", DefiLlama: "MegaETH", Mobula: "MegaETH", NativeSymbol: "ETH"},
{Slug: "sonic", DefiLlama: "Sonic", Mobula: "Sonic", NativeSymbol: "S"},
// Gnosis gas is xDAI but the site strip labels the native token GNO,
// so we publish GNO to match what the page displays. Mobula indexes
// the chain under its legacy XDAI name.
{Slug: "gnosis", DefiLlama: "Gnosis", Mobula: "XDAI", NativeSymbol: "GNO"},
{Slug: "celo", DefiLlama: "Celo", Mobula: "Celo", NativeSymbol: "CELO"},
{Slug: "moonbeam", DefiLlama: "Moonbeam", Mobula: "Moonbeam", NativeSymbol: "GLMR"},
{Slug: "unichain", DefiLlama: "Unichain", Mobula: "", NativeSymbol: "ETH"},
{Slug: "berachain", DefiLlama: "Berachain", Mobula: "Berachain", NativeSymbol: "BERA"},
{Slug: "cronos", DefiLlama: "Cronos", Mobula: "Cronos", NativeSymbol: "CRO"},
// Fraxtal gas is frxETH, so we follow the same gas-token convention as
// the ETH rollups. Mobula symbol FRAX resolves to the legacy Frax
// stablecoin (~$1), the wrong asset for a native-token card.
{Slug: "fraxtal", DefiLlama: "Fraxtal", Mobula: "", NativeSymbol: "FRXETH"},
{Slug: "soneium", DefiLlama: "Soneium", Mobula: "", NativeSymbol: "ETH"},
}
Loading