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
69 changes: 69 additions & 0 deletions benchmarks/memecoin-platforms.yml
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions harnesses/memecoin-platforms/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
120 changes: 120 additions & 0 deletions harnesses/memecoin-platforms/cmd/monitor/main.go
Original file line number Diff line number Diff line change
@@ -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()
}
43 changes: 43 additions & 0 deletions harnesses/memecoin-platforms/cmd/monitor/metrics.go
Original file line number Diff line number Diff line change
@@ -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",
})
)
51 changes: 51 additions & 0 deletions harnesses/memecoin-platforms/cmd/monitor/mobula.go
Original file line number Diff line number Diff line change
@@ -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
}
51 changes: 51 additions & 0 deletions harnesses/memecoin-platforms/cmd/monitor/pumpfun.go
Original file line number Diff line number Diff line change
@@ -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)
}
19 changes: 19 additions & 0 deletions harnesses/memecoin-platforms/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
Loading
Loading