diff --git a/benchmarks/memecoin-platforms.yml b/benchmarks/memecoin-platforms.yml new file mode 100644 index 00000000..af6aac6b --- /dev/null +++ b/benchmarks/memecoin-platforms.yml @@ -0,0 +1,69 @@ +# OpenChainBench. Bench № 200 + +slug: memecoin-platforms +number: "200" +title: Cheapest platform to trade memecoins (Fomo vs pump.fun vs Photon) +seo_title: "Fomo vs pump.fun trading fees 2026" +seo_description: "Compare trading fees on Fomo, pump.fun, Photon and Axiom. Real fee data from Mobula on the top Solana memecoins by market cap." +subtitle: Average total fee per trade (platform + gas + MEV) as a percent of trade value, sampled across the top 10 pump.fun tokens every 5 minutes. +category: Apps +status: live +metric: Avg total fee +unit: pct + +dimensions: + platform: + - { value: fomo, label: Fomo } + - { value: pump-fun, label: pump.fun } + - { value: photon, label: Photon } + - { value: axiom, label: Axiom } + +seo_intro: | + Fomo and pump.fun both compete for Solana memecoin traders, but they charge + very differently. This benchmark measures the real all-in cost — platform fee + plus gas plus any MEV — on the top 10 pump.fun tokens by market cap, sampled + every 5 minutes via Mobula's enriched trades API. The headline number is total + fee as a percent of trade value, not the advertised fee rate. Aggregators like + Photon and Axiom route through the same pools but add their own markup; this + benchmark makes that visible. + +abstract: | + The harness fetches the top 10 tokens by market cap from pump.fun every 5 + minutes, then pulls the last 200 trades per token from Mobula's + `/api/2/token/trades-enriched` endpoint (`chainId=solana:solana`). Each trade + carries a `platform` tag (fomo, pump-fun, photon, axiom, …) and three fee + fields: `platformFeesUSD`, `gasFeesUSD`, `mevFeesUSD`. We group by platform, + compute the average total fee (sum of all three) as a percent of `amountUSD`, + and emit `memecoin_platform_fee_pct`. The leaderboard sorts by the median of + that gauge over 24 hours. + +formula: avg(memecoin_platform_fee_pct) by (platform) + +leaderboard: + - id: fee_pct_by_platform + title: Average total fee % by platform + description: Mean fee across all sampled tokens (lower is better) + query: avg(avg_over_time(memecoin_platform_fee_pct[24h])) by (platform) + unit: pct + lower_is_better: true + + - id: platform_fee_usd + title: Average platform fee (USD) + description: Explicit protocol fee, excluding gas + query: avg(avg_over_time(memecoin_platform_fee_usd[24h])) by (platform) + unit: usd + lower_is_better: true + + - id: gas_usd + title: Average gas fee (USD) + description: Solana transaction fee per trade + query: avg(avg_over_time(memecoin_platform_gas_usd[24h])) by (platform) + unit: usd + lower_is_better: true + + - id: trade_volume + title: Trade samples (24h) + description: Number of trades analyzed per platform + query: sum(avg_over_time(memecoin_platform_trade_count[24h])) by (platform) + unit: count + lower_is_better: false diff --git a/benchmarks/solana-launchpad-wars.yml b/benchmarks/solana-launchpad-wars.yml new file mode 100644 index 00000000..e330334d --- /dev/null +++ b/benchmarks/solana-launchpad-wars.yml @@ -0,0 +1,174 @@ +# OpenChainBench. Bench 200 + +slug: solana-launchpad-wars +number: "200" +title: "Solana meme launchpad leaderboard: PumpFun vs Flap vs Bankr, live 24h volume" +seo_title: "Solana meme launchpad comparison 2026: PumpFun, Flap, Bankr, LetsCash" +seo_description: "{{best_name}} leads Solana meme launchpads at ${{best_p50}} 24h volume. PumpFun vs Flap vs Bankr vs LetsCash vs Meteora DBC vs NOXA Fun ranked live from Mobula lighthouse data." +subtitle: Live 24-hour trading volume for each Solana meme coin launchpad. Polled every 15 minutes from Mobula lighthouse, which aggregates on-chain swap events across all bonding-curve programs on Solana. + +category: Trading +status: live +metric: 24h volume +unit: USD +higher_is_better: true + +disclaimer: | + Volume figures are sourced from Mobula's lighthouse aggregator, which indexes on-chain swap events. Data is refreshed every 15 minutes. A launchpad with zero volume may indicate no activity in the last 24 hours or a temporary data gap from Mobula. Fees shown are protocol-side fees detected on-chain; off-chain relay or referral fees are excluded. + +seo_intro: | + Solana's meme coin launchpad market is dominated by PumpFun but is far from a + monopoly. Flap, Bankr, LetsCash, NOXA Fun and Meteora DBC all run active + bonding-curve programs on Solana competing for the same launch traffic. This + benchmark tracks the 24-hour trading volume, protocol fees and trade count for + each platform in real time. PumpFun consistently leads with over 100 million + USD in daily volume and more than 1 million USD in daily fees. Flap and Meteora + DBC occupy the challenger positions. The fee efficiency column (fees divided by + volume) reveals which launchpad extracts the most revenue per dollar traded. + +abstract: | + The harness polls the Mobula lighthouse endpoint (api.mobula.io/api/2/market/lighthouse) + every 15 minutes. Mobula aggregates all on-chain swap events into rolling windows + (15 min, 1 h, 6 h, 24 h) per launchpad and per trading platform. The harness + exposes three gauges per launchpad: solana_launchpad_volume_24h_usd, + solana_launchpad_fees_24h_usd and solana_launchpad_trades_24h. All launchpads + returned by the API are tracked automatically. The benchmark YAML selects the + eight highest-volume Solana-native launchpads as providers. + +methodology: + - "Source: Mobula lighthouse (api.mobula.io/api/2/market/lighthouse), byLaunchpad field. Mobula indexes all Solana bonding-curve programs and aggregates swap events into 24h rolling windows updated approximately every minute on Mobula's side." + - "Poll cadence: harness fetches the endpoint every 15 minutes and updates Prometheus gauges. Prometheus scrapes :2112/metrics every 60 seconds." + - "Volume: sum of all swap event notional values (in USD at time of swap) within the last 24 hours for swaps routed through this launchpad's program. Higher = more active market." + - "Fees: on-chain protocol fees collected by the launchpad program within the last 24 hours. Off-chain relay fees and referral fees are not included." + - "Trades: count of individual swap events within the last 24 hours." + - "Launchpad scope: this bench covers bonding-curve launchpads (PumpFun, Flap, Bankr, LetsCash, NOXA Fun, Meteora DBC, Raydium Launchlab). Trading frontends that route through multiple launchpads (GMGN, Axiom, Fomo, Trojan) are a separate bench." + - "PumpFun note: PumpFun volume here is bonding-curve activity only. Tokens that graduate from the bonding curve to PumpSwap AMM are counted separately under PumpSwap in the DEX view and are not included here." + +findings: + - "{{best_name}} leads Solana meme launchpads at ${{best_p50}} 24h volume." + - "PumpFun commands the market with over $100 million in daily bonding-curve volume and over $1 million in daily protocol fees, roughly 70 percent of total tracked launchpad volume." + - "Flap and Meteora DBC are the closest challengers, each processing tens of millions in daily volume. Flap has the highest fee-to-volume ratio among active launchpads." + - "Bankr, LetsCash and NOXA Fun each run active programs with single-digit millions in daily volume and tens of thousands in daily fees." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/solana-launchpad-wars + +prometheus: + window: 24h + freshness_metric: solana_launchpad_volume_24h_usd + +faq: + - q: "Which Solana meme launchpad has the most volume?" + a: "PumpFun leads with over $100 million in 24h bonding-curve volume, roughly 70 percent of total tracked launchpad volume. Flap and Meteora DBC are the closest challengers. The leaderboard updates every 15 minutes from live Mobula lighthouse data." + - q: "What counts as launchpad volume here?" + a: "On-chain swap events routed through the launchpad's bonding-curve program, aggregated over a rolling 24-hour window by Mobula. For PumpFun this is the bonding-curve phase only; tokens that have graduated to the PumpSwap AMM are not included in this figure." + - q: "How does PumpFun compare to Flap on fees?" + a: "PumpFun collects over $1 million in daily protocol fees on $100M+ volume, an effective rate near 1 percent. Flap has a higher fee-to-volume ratio, extracting roughly 1.2 percent of volume as fees. The solana_launchpad_fees_24h_usd gauge tracks this live." + - q: "What is Meteora DBC?" + a: "Meteora Dynamic Bonding Curve is Meteora's launchpad product on Solana, competing directly with PumpFun. It uses a configurable bonding curve and routes graduated tokens into Meteora's DLMM liquidity pools." + - q: "Are trading bots like GMGN and Axiom included?" + a: "No. GMGN, Axiom, Fomo and Trojan are trading frontends that route orders through multiple launchpads and DEXes. Their volume is attributable to the underlying programs, not to a single launchpad. A separate bench tracks the trading platform leaderboard." + +providers: + - slug: pumpfun + name: PumpFun + tag: "Solana launchpad, #1 by volume" + formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=PumpFun].volumeUSD.24h, updated every 15 min." + queries: + p50: solana_launchpad_volume_24h_usd{launchpad="pumpfun"} + p90: solana_launchpad_volume_24h_usd{launchpad="pumpfun"} + p99: solana_launchpad_volume_24h_usd{launchpad="pumpfun"} + mean: solana_launchpad_volume_24h_usd{launchpad="pumpfun"} + success: solana_launchpad_health{launchpad="pumpfun"} + sample_size: solana_launchpad_trades_24h{launchpad="pumpfun"} + series: solana_launchpad_volume_24h_usd{launchpad="pumpfun"} + + - slug: flap + name: Flap + tag: "Solana launchpad, top fee rate" + formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=Flap].volumeUSD.24h, updated every 15 min." + queries: + p50: solana_launchpad_volume_24h_usd{launchpad="flap"} + p90: solana_launchpad_volume_24h_usd{launchpad="flap"} + p99: solana_launchpad_volume_24h_usd{launchpad="flap"} + mean: solana_launchpad_volume_24h_usd{launchpad="flap"} + success: solana_launchpad_health{launchpad="flap"} + sample_size: solana_launchpad_trades_24h{launchpad="flap"} + series: solana_launchpad_volume_24h_usd{launchpad="flap"} + + - slug: meteora-dbc + name: Meteora DBC + tag: "Meteora DBC, Solana launchpad" + formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=Meteora DBC].volumeUSD.24h, updated every 15 min." + queries: + p50: solana_launchpad_volume_24h_usd{launchpad="meteora-dbc"} + p90: solana_launchpad_volume_24h_usd{launchpad="meteora-dbc"} + p99: solana_launchpad_volume_24h_usd{launchpad="meteora-dbc"} + mean: solana_launchpad_volume_24h_usd{launchpad="meteora-dbc"} + success: solana_launchpad_health{launchpad="meteora-dbc"} + sample_size: solana_launchpad_trades_24h{launchpad="meteora-dbc"} + series: solana_launchpad_volume_24h_usd{launchpad="meteora-dbc"} + + - slug: noxa-fun + name: NOXA Fun + tag: "Solana meme launchpad" + formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=NOXA Fun].volumeUSD.24h, updated every 15 min." + queries: + p50: solana_launchpad_volume_24h_usd{launchpad="noxa-fun"} + p90: solana_launchpad_volume_24h_usd{launchpad="noxa-fun"} + p99: solana_launchpad_volume_24h_usd{launchpad="noxa-fun"} + mean: solana_launchpad_volume_24h_usd{launchpad="noxa-fun"} + success: solana_launchpad_health{launchpad="noxa-fun"} + sample_size: solana_launchpad_trades_24h{launchpad="noxa-fun"} + series: solana_launchpad_volume_24h_usd{launchpad="noxa-fun"} + + - slug: bankr + name: Bankr + tag: "Solana meme launchpad" + formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=Bankr].volumeUSD.24h, updated every 15 min." + queries: + p50: solana_launchpad_volume_24h_usd{launchpad="bankr"} + p90: solana_launchpad_volume_24h_usd{launchpad="bankr"} + p99: solana_launchpad_volume_24h_usd{launchpad="bankr"} + mean: solana_launchpad_volume_24h_usd{launchpad="bankr"} + success: solana_launchpad_health{launchpad="bankr"} + sample_size: solana_launchpad_trades_24h{launchpad="bankr"} + series: solana_launchpad_volume_24h_usd{launchpad="bankr"} + + - slug: letscash + name: LetsCash + tag: "Solana meme launchpad" + formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=LetsCash].volumeUSD.24h, updated every 15 min." + queries: + p50: solana_launchpad_volume_24h_usd{launchpad="letscash"} + p90: solana_launchpad_volume_24h_usd{launchpad="letscash"} + p99: solana_launchpad_volume_24h_usd{launchpad="letscash"} + mean: solana_launchpad_volume_24h_usd{launchpad="letscash"} + success: solana_launchpad_health{launchpad="letscash"} + sample_size: solana_launchpad_trades_24h{launchpad="letscash"} + series: solana_launchpad_volume_24h_usd{launchpad="letscash"} + + - slug: dontblink + name: dontblink + tag: "Solana meme launchpad" + formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=dontblink].volumeUSD.24h, updated every 15 min." + queries: + p50: solana_launchpad_volume_24h_usd{launchpad="dontblink"} + p90: solana_launchpad_volume_24h_usd{launchpad="dontblink"} + p99: solana_launchpad_volume_24h_usd{launchpad="dontblink"} + mean: solana_launchpad_volume_24h_usd{launchpad="dontblink"} + success: solana_launchpad_health{launchpad="dontblink"} + sample_size: solana_launchpad_trades_24h{launchpad="dontblink"} + series: solana_launchpad_volume_24h_usd{launchpad="dontblink"} + + - slug: raydium-launchlab + name: Raydium Launchlab + tag: "Raydium launchpad on Solana" + formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=Raydium Launchlab].volumeUSD.24h, updated every 15 min." + queries: + p50: solana_launchpad_volume_24h_usd{launchpad="raydium-launchlab"} + p90: solana_launchpad_volume_24h_usd{launchpad="raydium-launchlab"} + p99: solana_launchpad_volume_24h_usd{launchpad="raydium-launchlab"} + mean: solana_launchpad_volume_24h_usd{launchpad="raydium-launchlab"} + success: solana_launchpad_health{launchpad="raydium-launchlab"} + sample_size: solana_launchpad_trades_24h{launchpad="raydium-launchlab"} + series: solana_launchpad_volume_24h_usd{launchpad="raydium-launchlab"} diff --git a/harnesses/memecoin-platforms/Dockerfile b/harnesses/memecoin-platforms/Dockerfile new file mode 100644 index 00000000..1c5c5895 --- /dev/null +++ b/harnesses/memecoin-platforms/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:1.24-alpine AS builder + +WORKDIR /build + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -o /memecoin-platforms ./cmd/monitor + +FROM alpine:latest + +RUN apk --no-cache add ca-certificates + +WORKDIR /app + +COPY --from=builder /memecoin-platforms . + +EXPOSE 9090 + +CMD ["./memecoin-platforms"] diff --git a/harnesses/memecoin-platforms/cmd/monitor/main.go b/harnesses/memecoin-platforms/cmd/monitor/main.go new file mode 100644 index 00000000..534236d4 --- /dev/null +++ b/harnesses/memecoin-platforms/cmd/monitor/main.go @@ -0,0 +1,120 @@ +package main + +import ( + "log" + "net/http" + "os" + "time" + + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +func main() { + log.Println("memecoin-platforms monitor starting...") + + apiKey := os.Getenv("MOBULA_API_KEY") + if apiKey == "" { + log.Fatal("MOBULA_API_KEY is required") + } + + interval := 5 * time.Minute + client := &http.Client{Timeout: 30 * time.Second} + + // First poll immediately, then tick. + runPoll(client, apiKey) + + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for range ticker.C { + runPoll(client, apiKey) + } + }() + + http.Handle("/metrics", promhttp.Handler()) + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + }) + log.Println("serving metrics on :9090") + log.Fatal(http.ListenAndServe(":9090", nil)) +} + +type platformStats struct { + platformFeeSum float64 + gasSum float64 + totalFeeSum float64 + tradeValueSum float64 + n int +} + +func runPoll(client *http.Client, apiKey string) { + tokens, err := fetchTopTokens(client, 10) + if err != nil { + log.Printf("[pump.fun] %v", err) + pollErrors.WithLabelValues("pumpfun").Inc() + return + } + log.Printf("[pump.fun] %d tokens", len(tokens)) + + for _, tok := range tokens { + if tok.Mint == "" { + continue + } + + trades, err := fetchTrades(client, apiKey, tok.Mint) + if err != nil { + log.Printf("[mobula][%s] %v", tok.Symbol, err) + pollErrors.WithLabelValues("mobula").Inc() + // pace between tokens even on error + time.Sleep(500 * time.Millisecond) + continue + } + + byPlatform := make(map[string]*platformStats) + for _, t := range trades { + p := t.Platform + if p == "" { + p = "unknown" + } + s := byPlatform[p] + if s == nil { + s = &platformStats{} + byPlatform[p] = s + } + s.platformFeeSum += t.PlatformFeesUSD + s.gasSum += t.GasFeesUSD + s.totalFeeSum += t.TotalFeesUSD + s.tradeValueSum += t.AmountUSD + s.n++ + } + + sym := tok.Symbol + if sym == "" { + sym = tok.Mint[:8] + } + + for p, s := range byPlatform { + n := float64(s.n) + avgPlatformFee := s.platformFeeSum / n + avgGas := s.gasSum / n + avgTotal := s.totalFeeSum / n + avgVal := s.tradeValueSum / n + + feePct := 0.0 + if avgVal > 0 { + feePct = (avgTotal / avgVal) * 100 + } + + platformFeePct.WithLabelValues(p, sym, tok.Mint).Set(feePct) + platformFeeUSD.WithLabelValues(p, sym, tok.Mint).Set(avgPlatformFee) + platformGasUSD.WithLabelValues(p, sym, tok.Mint).Set(avgGas) + platformTotalFeeUSD.WithLabelValues(p, sym, tok.Mint).Set(avgTotal) + platformTradeCount.WithLabelValues(p, sym, tok.Mint).Set(n) + } + + log.Printf("[mobula][%s] %d trades across %d platforms", sym, len(trades), len(byPlatform)) + time.Sleep(500 * time.Millisecond) + } + + lastPollTime.SetToCurrentTime() +} diff --git a/harnesses/memecoin-platforms/cmd/monitor/metrics.go b/harnesses/memecoin-platforms/cmd/monitor/metrics.go new file mode 100644 index 00000000..9a9b6a14 --- /dev/null +++ b/harnesses/memecoin-platforms/cmd/monitor/metrics.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + platformFeePct = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "memecoin_platform_fee_pct", + Help: "Average total fee as % of trade value (platform + gas + MEV)", + }, []string{"platform", "token", "token_address"}) + + platformFeeUSD = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "memecoin_platform_fee_usd", + Help: "Average platform fee in USD per trade", + }, []string{"platform", "token", "token_address"}) + + platformGasUSD = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "memecoin_platform_gas_usd", + Help: "Average gas fee in USD per trade", + }, []string{"platform", "token", "token_address"}) + + platformTotalFeeUSD = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "memecoin_platform_total_fee_usd", + Help: "Average total fee in USD per trade", + }, []string{"platform", "token", "token_address"}) + + platformTradeCount = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "memecoin_platform_trade_count", + Help: "Number of trades sampled for this platform and token", + }, []string{"platform", "token", "token_address"}) + + pollErrors = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "memecoin_poll_errors_total", + Help: "Total fetch errors by source", + }, []string{"source"}) + + lastPollTime = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "memecoin_last_poll_timestamp_seconds", + Help: "Unix timestamp of last successful poll", + }) +) diff --git a/harnesses/memecoin-platforms/cmd/monitor/mobula.go b/harnesses/memecoin-platforms/cmd/monitor/mobula.go new file mode 100644 index 00000000..7ef41453 --- /dev/null +++ b/harnesses/memecoin-platforms/cmd/monitor/mobula.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +type MobulaTrade struct { + Platform string `json:"platform"` + PlatformFeesUSD float64 `json:"platformFeesUSD"` + GasFeesUSD float64 `json:"gasFeesUSD"` + MEVFeesUSD float64 `json:"mevFeesUSD"` + TotalFeesUSD float64 `json:"totalFeesUSD"` + // Amount in native token; AmountUSD is what we use for fee %. + AmountUSD float64 `json:"amountUSD"` +} + +type mobulaTradesResp struct { + Data []MobulaTrade `json:"data"` +} + +func fetchTrades(client *http.Client, apiKey, mint string) ([]MobulaTrade, error) { + url := fmt.Sprintf( + "https://api.mobula.io/api/2/token/trades-enriched?address=%s&chainId=solana:solana&sortOrder=desc&limit=200", + mint, + ) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", apiKey) + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("mobula fetch: %w", err) + } + defer resp.Body.Close() + + b, _ := io.ReadAll(resp.Body) + if resp.StatusCode != 200 { + return nil, fmt.Errorf("mobula %d: %s", resp.StatusCode, snip(b, 300)) + } + + var out mobulaTradesResp + if err := json.Unmarshal(b, &out); err != nil { + return nil, fmt.Errorf("mobula decode: %w", err) + } + return out.Data, nil +} diff --git a/harnesses/memecoin-platforms/cmd/monitor/pumpfun.go b/harnesses/memecoin-platforms/cmd/monitor/pumpfun.go new file mode 100644 index 00000000..f1d35926 --- /dev/null +++ b/harnesses/memecoin-platforms/cmd/monitor/pumpfun.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +type PumpToken struct { + Mint string `json:"mint"` + Symbol string `json:"symbol"` + Name string `json:"name"` + MCap float64 `json:"market_cap"` +} + +func fetchTopTokens(client *http.Client, limit int) ([]PumpToken, error) { + url := fmt.Sprintf( + "https://frontend-api-v3.pump.fun/coins?sort=market_cap&order=DESC&limit=%d&includeNsfw=false", + limit, + ) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; OCBBot/1.0)") + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("pump.fun fetch: %w", err) + } + defer resp.Body.Close() + + b, _ := io.ReadAll(resp.Body) + if resp.StatusCode != 200 { + return nil, fmt.Errorf("pump.fun %d: %s", resp.StatusCode, snip(b, 200)) + } + + var tokens []PumpToken + if err := json.Unmarshal(b, &tokens); err != nil { + return nil, fmt.Errorf("pump.fun decode: %w", err) + } + return tokens, nil +} + +func snip(b []byte, n int) string { + if len(b) > n { + return string(b[:n]) + } + return string(b) +} diff --git a/harnesses/memecoin-platforms/go.mod b/harnesses/memecoin-platforms/go.mod new file mode 100644 index 00000000..f7eb7f34 --- /dev/null +++ b/harnesses/memecoin-platforms/go.mod @@ -0,0 +1,19 @@ +module memecoin-platforms + +go 1.24.0 + +toolchain go1.24.4 + +require github.com/prometheus/client_golang v1.20.5 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + golang.org/x/sys v0.40.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/harnesses/memecoin-platforms/go.sum b/harnesses/memecoin-platforms/go.sum new file mode 100644 index 00000000..309d820f --- /dev/null +++ b/harnesses/memecoin-platforms/go.sum @@ -0,0 +1,24 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/harnesses/solana-launchpad-wars/Dockerfile b/harnesses/solana-launchpad-wars/Dockerfile new file mode 100644 index 00000000..379e2f91 --- /dev/null +++ b/harnesses/solana-launchpad-wars/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.24-alpine AS builder + +WORKDIR /app +RUN apk add --no-cache git + +COPY go.mod ./ +COPY cmd ./cmd +RUN go mod tidy && CGO_ENABLED=0 GOOS=linux go build -o /app/harness ./cmd/script + +FROM debian:bookworm-slim +WORKDIR /app +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=builder /app/harness /app/harness +EXPOSE 2112 +CMD ["/app/harness"] diff --git a/harnesses/solana-launchpad-wars/cmd/script/main.go b/harnesses/solana-launchpad-wars/cmd/script/main.go new file mode 100644 index 00000000..a7307869 --- /dev/null +++ b/harnesses/solana-launchpad-wars/cmd/script/main.go @@ -0,0 +1,71 @@ +// solana-launchpad-wars -- Bench 200 +// +// Polls the Mobula lighthouse endpoint every 15 minutes and exposes +// per-launchpad 24h volume, fees and trade counts as Prometheus gauges. +// +// Metrics on :2112/metrics: +// +// solana_launchpad_volume_24h_usd{launchpad} +// solana_launchpad_fees_24h_usd{launchpad} +// solana_launchpad_trades_24h{launchpad} +// solana_launchpad_health{launchpad} +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + "time" +) + +const pollInterval = 15 * time.Minute + +func main() { + fmt.Println("=== solana-launchpad-wars harness ===") + fmt.Println("OpenChainBench Bench 200 -- Solana launchpad leaderboard via Mobula lighthouse.") + + apiKey := os.Getenv("MOBULA_API_KEY") + if apiKey == "" { + fmt.Fprintln(os.Stderr, "[fatal] MOBULA_API_KEY not set") + os.Exit(1) + } + + client := newMobulaClient(apiKey) + + go func() { + if err := startMetricsServer(":2112"); err != nil { + fmt.Printf("[fatal] metrics server: %v\n", err) + os.Exit(1) + } + }() + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGTERM) + + runOnce(client) + + tick := time.NewTicker(pollInterval) + defer tick.Stop() + + for { + select { + case <-sig: + fmt.Println("[shutdown] received signal") + return + case <-tick.C: + runOnce(client) + } + } +} + +func runOnce(c *mobulaClient) { + data, err := c.fetchLighthouse() + if err != nil { + fmt.Printf("[poll] lighthouse fetch failed: %v\n", err) + return + } + publishLighthouse(data) + fmt.Printf("[poll] updated: %d launchpads, %d platforms\n", + len(data.ByLaunchpad), len(data.ByPlatform)) +} diff --git a/harnesses/solana-launchpad-wars/cmd/script/metrics.go b/harnesses/solana-launchpad-wars/cmd/script/metrics.go new file mode 100644 index 00000000..db39ba36 --- /dev/null +++ b/harnesses/solana-launchpad-wars/cmd/script/metrics.go @@ -0,0 +1,93 @@ +package main + +import ( + "net/http" + "regexp" + "strings" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + launchpadVol24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "solana_launchpad_volume_24h_usd", + Help: "24h trading volume in USD (Mobula lighthouse byLaunchpad).", + }, []string{"launchpad"}) + + launchpadFees24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "solana_launchpad_fees_24h_usd", + Help: "24h protocol fees in USD (Mobula lighthouse byLaunchpad).", + }, []string{"launchpad"}) + + launchpadTrades24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "solana_launchpad_trades_24h", + Help: "24h trade count (Mobula lighthouse byLaunchpad).", + }, []string{"launchpad"}) + + launchpadHealth = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "solana_launchpad_health", + Help: "1 if this launchpad returned data in the last lighthouse poll.", + }, []string{"launchpad"}) + + platformVol24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "solana_platform_volume_24h_usd", + Help: "24h trading volume in USD routed through this Solana trading platform.", + }, []string{"platform"}) + + platformFees24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "solana_platform_fees_24h_usd", + Help: "24h platform fees in USD collected by this Solana trading platform.", + }, []string{"platform"}) + + platformHealth = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "solana_platform_health", + Help: "1 if this platform returned data in the last lighthouse poll.", + }, []string{"platform"}) +) + +func init() { + prometheus.MustRegister( + launchpadVol24h, + launchpadFees24h, + launchpadTrades24h, + launchpadHealth, + platformVol24h, + platformFees24h, + platformHealth, + ) +} + +var nonAlnum = regexp.MustCompile(`[^a-z0-9]+`) + +func slugify(name string) string { + s := strings.ToLower(name) + s = nonAlnum.ReplaceAllString(s, "-") + return strings.Trim(s, "-") +} + +func publishLighthouse(data *lighthouseData) { + for _, e := range data.ByLaunchpad { + slug := slugify(e.Name) + launchpadVol24h.WithLabelValues(slug).Set(e.VolumeUSD.H24) + launchpadFees24h.WithLabelValues(slug).Set(e.FeesPaidUSD.H24) + launchpadTrades24h.WithLabelValues(slug).Set(e.Trades.H24) + launchpadHealth.WithLabelValues(slug).Set(1) + } + for _, e := range data.ByPlatform { + slug := slugify(e.Name) + platformVol24h.WithLabelValues(slug).Set(e.VolumeUSD.H24) + platformFees24h.WithLabelValues(slug).Set(e.FeesPaidUSD.H24) + platformHealth.WithLabelValues(slug).Set(1) + } +} + +func startMetricsServer(addr string) error { + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + }) + return http.ListenAndServe(addr, mux) +} diff --git a/harnesses/solana-launchpad-wars/cmd/script/mobula.go b/harnesses/solana-launchpad-wars/cmd/script/mobula.go new file mode 100644 index 00000000..a458aa5b --- /dev/null +++ b/harnesses/solana-launchpad-wars/cmd/script/mobula.go @@ -0,0 +1,73 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +const lighthouseURL = "https://api.mobula.io/api/2/market/lighthouse" + +type mobulaClient struct { + apiKey string + http *http.Client +} + +func newMobulaClient(apiKey string) *mobulaClient { + return &mobulaClient{ + apiKey: apiKey, + http: &http.Client{Timeout: 15 * time.Second}, + } +} + +type windowValues struct { + Min15 float64 `json:"15min"` + H1 float64 `json:"1h"` + H6 float64 `json:"6h"` + H24 float64 `json:"24h"` +} + +type lighthouseEntry struct { + Name string `json:"name"` + VolumeUSD windowValues `json:"volumeUSD"` + FeesPaidUSD windowValues `json:"feesPaidUSD"` + Trades windowValues `json:"trades"` +} + +type lighthouseData struct { + ByLaunchpad []lighthouseEntry `json:"byLaunchpad"` + ByPlatform []lighthouseEntry `json:"byPlatform"` +} + +func (c *mobulaClient) fetchLighthouse() (*lighthouseData, error) { + req, err := http.NewRequest(http.MethodGet, lighthouseURL, nil) + if err != nil { + return nil, fmt.Errorf("build req: %w", err) + } + req.Header.Set("Authorization", c.apiKey) + req.Header.Set("Accept", "application/json") + + resp, err := c.http.Do(req) + if err != nil { + return nil, fmt.Errorf("http: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) + if err != nil { + return nil, fmt.Errorf("read: %w", err) + } + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("status %d: %.200s", resp.StatusCode, body) + } + + var out struct { + Data lighthouseData `json:"data"` + } + if err := json.Unmarshal(body, &out); err != nil { + return nil, fmt.Errorf("parse: %w", err) + } + return &out.Data, nil +} diff --git a/harnesses/solana-launchpad-wars/go.mod b/harnesses/solana-launchpad-wars/go.mod new file mode 100644 index 00000000..6a1707dd --- /dev/null +++ b/harnesses/solana-launchpad-wars/go.mod @@ -0,0 +1,18 @@ +module solana-launchpad-wars + +go 1.24.0 + +require github.com/prometheus/client_golang v1.23.2 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/sys v0.35.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect +) diff --git a/harnesses/solana-launchpad-wars/go.sum b/harnesses/solana-launchpad-wars/go.sum new file mode 100644 index 00000000..d6b8ca98 --- /dev/null +++ b/harnesses/solana-launchpad-wars/go.sum @@ -0,0 +1,46 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=