diff --git a/benchmarks/solana-dex-volume.yml b/benchmarks/solana-dex-volume.yml index 2c926edfc..5d7264322 100644 --- a/benchmarks/solana-dex-volume.yml +++ b/benchmarks/solana-dex-volume.yml @@ -16,6 +16,10 @@ higher_is_better: true disclaimer: | Volume and revenue data sourced from DeFiLlama's DEX and fees APIs. DeFiLlama attributes volume via on-chain referral tags and program IDs; platforms without standard tags may be undercounted. Figures are 24-hour and 7-day rolling totals updated every 30 minutes. Revenue uses DeFiLlama's dailyRevenue dataType, stripping LP fees. Terminal volume (Axiom, GMGN, Fomo) and launchpad volume (pump.fun) use different attribution methods and are not directly additive. +provider_notes: + gmgn: "Solana only" + fomo: "on-chain swaps only" + seo_intro: | The Solana DEX market in summer 2026 is split between pump.fun's launchpad, which dominates early-stage token trading, and a set of competing terminals @@ -38,9 +42,10 @@ abstract: | defillama_dex_take_rate and defillama_dex_health. methodology: - - "Source: DeFiLlama public DEX API (api.llama.fi/summary/dexs/{slug}) and fees API (api.llama.fi/summary/fees/{slug}?dataType=dailyRevenue). No authentication required." - - "Poll cadence: harness fetches each platform every 30 minutes and updates Prometheus gauges immediately." - - "Volume: total24h and total7d fields from DeFiLlama's DEX endpoint, sum of all swap notional values in USD attributed to this platform." + - "Source: DeFiLlama public chain overview APIs (api.llama.fi/overview/dexs/solana and overview/fees/solana?dataType=dailyRevenue), which report SOLANA-scoped totals per protocol. Multichain platforms such as GMGN therefore show their Solana volume only, keeping every row comparable. No authentication required." + - "Poll cadence: harness fetches every 30 minutes and updates Prometheus gauges immediately." + - "Fomo caveat: DeFiLlama attributes Fomo volume from on-chain Solana swaps matched to its fee wallet, while its revenue figure also includes relay fees the Fomo team self-reports. Fomo's displayed revenue-to-volume ratio (~5%) versus its stated 0.5% fee implies substantially higher total activity than the on-chain volume shown here; its Hyperliquid perps flow is tracked separately on the hyperliquid-frontends bench." + - "Volume: total24h and total7d fields, sum of swap notional values in USD attributed to this platform on Solana. DeFiLlama aggregates in daily buckets with roughly a 10-hour indexing delay, so intraday values are flat by construction and step once per day." - "Revenue: total24h and total7d from the fees endpoint with dataType=dailyRevenue. This strips LP fees that stay in pool accounts and returns only the protocol's own share. Note: dailyRevenue also nets out referral paybacks (e.g. GMGN nets ~34% back to affiliates) and for pump.fun reflects the buyback-adjusted split introduced in 2026. Use dailyFees if comparing what users pay, not what protocols retain." - "Take rate: protocol revenue divided by trading volume (defillama_dex_fees_24h_usd / defillama_dex_volume_24h_usd). Measures how much of each dollar traded the platform captures." - "Attribution: DeFiLlama uses on-chain referral tags, program IDs and memo fields. Platforms without standard tagging may be undercounted." diff --git a/harnesses/solana-dex-volume/cmd/script/main.go b/harnesses/solana-dex-volume/cmd/script/main.go index e04d20498..18d017c9a 100644 --- a/harnesses/solana-dex-volume/cmd/script/main.go +++ b/harnesses/solana-dex-volume/cmd/script/main.go @@ -3,7 +3,11 @@ // Polls the DeFiLlama DEX API every 30 minutes for each tracked platform // and exposes per-platform 24h/7d volume and protocol revenue as Prometheus gauges. // -// No API key required. Endpoint: https://api.llama.fi/summary/dexs/{slug} +// No API key required. Primary source: https://api.llama.fi/overview/dexs/solana +// (chain-scoped, so multichain platforms like GMGN report their SOLANA +// volume only, keeping the bench apples-to-apples). Falls back to the +// per-protocol summary endpoint for any platform missing from the +// overview. // // Metrics on :2112/metrics: // @@ -22,6 +26,7 @@ import ( "net/http" "os" "os/signal" + "strings" "syscall" "time" @@ -36,16 +41,17 @@ const ( ) var platforms = []struct { - slug string - label string + slug string // per-protocol summary slug (fallback path) + label string // prometheus label + names []string // lowercase name candidates in the chain overview }{ - {"pump.fun", "pump-fun"}, - {"gmgn", "gmgn"}, - {"axiom", "axiom"}, - {"fomo-wallet", "fomo"}, - {"trojan", "trojan"}, - {"photon", "photon"}, - {"bullx", "bullx"}, + {"pump.fun", "pump-fun", []string{"pump.fun"}}, + {"gmgn", "gmgn", []string{"gmgn"}}, + {"axiom", "axiom", []string{"axiom"}}, + {"fomo-wallet", "fomo", []string{"fomo", "fomo-wallet"}}, + {"trojan", "trojan", []string{"trojan"}}, + {"photon", "photon", []string{"photon"}}, + {"bullx", "bullx", []string{"bullx"}}, } var ( @@ -85,6 +91,60 @@ type llamaResponse struct { Total7d float64 `json:"total7d"` } +type overviewResponse struct { + Protocols []struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Module string `json:"module"` + Total24h float64 `json:"total24h"` + Total7d float64 `json:"total7d"` + } `json:"protocols"` +} + +// fetchOverview returns Solana-scoped per-protocol totals indexed by +// lowercased name/displayName/module. One call covers every platform. +func fetchOverview(endpoint, query string) (map[string]llamaResponse, error) { + url := fmt.Sprintf("%s/overview/%s/solana?excludeTotalDataChart=true&excludeTotalDataChartBreakdown=true", baseURL, endpoint) + if query != "" { + url += "&" + query + } + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, fmt.Errorf("HTTP %d", resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var r overviewResponse + if err := json.Unmarshal(body, &r); err != nil { + return nil, err + } + out := map[string]llamaResponse{} + for _, p := range r.Protocols { + v := llamaResponse{Total24h: p.Total24h, Total7d: p.Total7d} + for _, k := range []string{p.Name, p.DisplayName, p.Module} { + if k != "" { + out[strings.ToLower(k)] = v + } + } + } + return out, nil +} + +func lookup(ov map[string]llamaResponse, names []string) (llamaResponse, bool) { + for _, n := range names { + if v, ok := ov[n]; ok { + return v, true + } + } + return llamaResponse{}, false +} + func fetch(endpoint, slug, query string) (llamaResponse, error) { url := fmt.Sprintf("%s/summary/%s/%s", baseURL, endpoint, slug) if query != "" { @@ -110,17 +170,37 @@ func fetch(endpoint, slug, query string) (llamaResponse, error) { } func runOnce() { + // Chain-scoped overviews first: one request each, Solana-only totals + // for every protocol. Errors degrade to the per-protocol fallback. + ovDex, errD := fetchOverview("dexs", "") + if errD != nil { + fmt.Printf("[poll] overview dexs: %v (falling back to summaries)\n", errD) + } + ovFees, errF := fetchOverview("fees", "dataType=dailyRevenue") + if errF != nil { + fmt.Printf("[poll] overview fees: %v (falling back to summaries)\n", errF) + } + for _, p := range platforms { - vol, err := fetch("dexs", p.slug, "") - if err != nil { - fmt.Printf("[poll] volume %s: %v\n", p.slug, err) - health.WithLabelValues(p.label).Set(0) - continue + vol, volOK := lookup(ovDex, p.names) + if !volOK { + v, err := fetch("dexs", p.slug, "") + if err != nil { + fmt.Printf("[poll] volume %s: %v\n", p.slug, err) + health.WithLabelValues(p.label).Set(0) + continue + } + fmt.Printf("[poll] %s: absent from solana overview, using all-chain summary\n", p.label) + vol = v } - rev, err := fetch("fees", p.slug, "dataType=dailyRevenue") - if err != nil { - fmt.Printf("[poll] fees %s: %v (volume ok)\n", p.slug, err) + rev, revOK := lookup(ovFees, p.names) + if !revOK { + r, err := fetch("fees", p.slug, "dataType=dailyRevenue") + if err != nil { + fmt.Printf("[poll] fees %s: %v (volume ok)\n", p.slug, err) + } + rev = r } volume24h.WithLabelValues(p.label).Set(vol.Total24h)