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/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=