diff --git a/benchmarks/memecoin-platforms.yml b/benchmarks/memecoin-platforms.yml index b02eead4..7fb5d828 100644 --- a/benchmarks/memecoin-platforms.yml +++ b/benchmarks/memecoin-platforms.yml @@ -1,11 +1,11 @@ # 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 fee plus gas plus MEV) as a percent of trade value, sampled across the top 10 pump.fun tokens every 5 minutes. +number: "203" +title: Cheapest platform to trade memecoins (pump.fun vs Fomo vs Photon vs bots) +seo_title: "Solana memecoin trading fees compared 2026" +seo_description: "Compare real on-chain trading fees across pump.fun, Fomo, Phantom, Photon, Axiom, Trojan and GMGN. Fees measured directly from Solana transactions on the top 10 pump.fun tokens." +subtitle: Average explicit on-chain fee per trade (gas + platform fee transfers) as a percent of trade value, sampled across the top 10 pump.fun tokens every 5 minutes. category: Trading status: live metric: Avg total fee @@ -14,63 +14,60 @@ unit: pct source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/memecoin-platforms 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. + Not all Solana memecoin frontends charge the same. This benchmark parses every + trade transaction directly from the Solana blockchain to measure what each + platform actually takes out of the user's pocket. Gas, explicit fee transfers to + platform wallets, and referral tips are all included. AMM LP fees that stay + inside pool accounts are excluded because they apply equally regardless of which + frontend you use. The result is the true all-in cost of hitting the buy or sell + button on each platform. 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 - token/trades-enriched endpoint (chainId=solana:solana). Each trade carries a - platform tag (fomo, pump-fun, photon, axiom, trojan, gmgn...) and three fee - fields: platformFeesUSD, gasFeesUSD, mevFeesUSD. We group by platform, compute - the average total fee as a percent of amountUSD, and emit - memecoin_platform_fee_pct. The leaderboard sorts by the median over 24 hours. + token/trades-enriched endpoint (chainId=solana:solana). For each trade carrying + a hash and sender address, the harness calls the public Solana RPC + (getTransaction, jsonParsed encoding) and computes explicit fees: Solana + transaction fee (gas), SOL transfers to known platform fee wallets, small SOL + recipients below 5% of user net flow, and small USDC/WSOL token recipients below + 5% of the largest pool receipt. Trades below $5 are excluded to avoid gas + dominating tiny amounts. Results are grouped by platform tag, averaged per + token, and emitted as memecoin_platform_fee_pct. methodology: - "Tokens: top 10 by market cap from pump.fun, refreshed every 5 minutes." - - "Trades: last 200 trades per token from Mobula token/trades-enriched (chainId=solana:solana)." - - "Cost: totalFeesUSD / amountUSD * 100, covering platform fee plus gas plus MEV." - - "Grouping: trades are split by the platform field returned by Mobula (fomo, pump-fun, photon, axiom, trojan, gmgn, maestro, phantom)." - - "Cadence: full sweep every 5 minutes; each poll covers the 200 most recent trades per token." - - "Aggregation: headline is the average of memecoin_platform_fee_pct across all tracked tokens over 24 hours." + - "Trades: last 200 trades per token from Mobula token/trades-enriched (chainId=solana:solana), filtered to amountUSD >= $5." + - "Fee source: Solana public RPC getTransaction (jsonParsed) for each trade hash. No third-party fee API." + - "Gas: meta.fee (lamports) converted to USD at the current SOL/USD price fetched from CoinGecko every 5 min." + - "Platform fees: explicit SOL or USDC/WSOL transfers to known fee wallet owners (confirmed on-chain). Unknown small recipients are captured via heuristics: SOL recipients receiving less than 5% of the user net flow, USDC/WSOL recipients receiving less than 5% of the largest pool receipt." + - "AMM LP fees excluded: the 0.20% PumpSwap fee and 0.25% Raydium fee stay inside pool accounts and are not counted because they apply equally to all frontends routing through the same pool." + - "Grouping: trades are split by the platform field returned by Mobula. Trades with no platform tag are assigned to pump-fun." + - "Cadence: full sweep every 5 minutes; tx results are cached to avoid re-parsing the same hash." + - "Aggregation: headline is the 24h median of memecoin_platform_fee_pct across all tracked tokens." findings: - "{{best_name}} currently leads with the lowest average trading fee at {{best_p50}} (p50, 24h) across top pump.fun tokens." - - "{{worst_name}} has the highest average fee at {{worst_p50}} (p50, 24h). Trading bots charge a fixed tip on top of DEX fees, which dominates at small trade sizes." + - "{{worst_name}} has the highest average fee at {{worst_p50}} (p50, 24h)." faq: - - q: "Is Fomo cheaper than pump.fun?" - a: "{{best_name}} currently leads at {{best_p50}} (p50, 24h). The live leaderboard refreshes every 5 minutes from Mobula trade data." + - q: "Are AMM LP fees included?" + a: "No. The 0.20% PumpSwap fee and 0.25% Raydium fee stay inside pool accounts and are not transferred anywhere. Since they apply equally to every frontend routing through the same pool, including them would not differentiate platforms. This benchmark measures only explicit transfers out of the user's wallet to platform or protocol fee recipients." - q: "What fees do memecoin trading platforms charge?" - a: "Native clients (pump.fun) charge a protocol fee on each swap. Aggregator frontends (Photon, Axiom) add a referral tip on top. Trading bots (Trojan, Maestro, GMGN) charge a fixed percentage per trade. Gas is a separate Solana transaction fee. This benchmark combines all three into a single all-in percent." - - q: "Does MEV affect Solana memecoin trading?" - a: "Minimally at this scale. Solana's parallel execution and fast finality reduce MEV extraction compared to EVM chains. The mevFeesUSD field in Mobula's data is near zero for most trades on these tokens." + a: "Native clients (pump.fun) collect a small protocol fee on each swap. Aggregator frontends (Photon, Axiom, Phantom) add a referral tip on top. Trading bots (Trojan, GMGN) charge a fixed percentage per trade sent to their fee wallet. Gas is a separate Solana transaction fee. This benchmark combines all of these into a single explicit all-in percent." + - q: "How are fee wallets identified?" + a: "For known platforms like Fomo, the fee wallet owner address is confirmed by inspecting tagged trades on-chain. For platforms without a confirmed wallet, the harness uses a heuristic: any non-system, non-pool account receiving less than 5% of the user's net SOL flow, or less than 5% of the largest token pool receipt, is counted as a fee recipient. This captures bot tips and referral fees without needing to hardcode every wallet." + - q: "Why is pump.fun near zero?" + a: "pump.fun's protocol fee goes to a program-owned pool account, not to an identifiable external wallet. Without a price oracle for the memecoin itself, the fee taken in the output token cannot be converted to USD. The benchmark measures explicit USD-denominated or SOL-denominated transfers; pump.fun's LP-style fee is currently outside that scope." prometheus: window: 24h expected_freshness_seconds: 900 providers: - - slug: fomo - name: Fomo - tag: Mobile app - formula: "Average total fee percent across top pump.fun tokens traded via Fomo, 24h median." - queries: - p50: quantile_over_time(0.50, avg(memecoin_platform_fee_pct{platform="fomo"})[24h:5m]) - p90: quantile_over_time(0.90, avg(memecoin_platform_fee_pct{platform="fomo"})[24h:5m]) - p99: quantile_over_time(0.99, avg(memecoin_platform_fee_pct{platform="fomo"})[24h:5m]) - mean: avg_over_time(avg(memecoin_platform_fee_pct{platform="fomo"})[24h:5m]) - sample_size: sum(avg_over_time(memecoin_platform_trade_count{platform="fomo"}[24h])) - - slug: pump-fun name: pump.fun tag: Native launchpad - formula: "Average total fee percent for trades on pump.fun bonding curve and its graduated Raydium pools, 24h median. Mobula does not tag pump.fun native trades with a platform id; they return platform:null and are counted here since we exclusively sample pump.fun top tokens." + formula: "Explicit on-chain fee percent for trades on pump.fun and PumpSwap pools, 24h median. Trades with no platform tag from Mobula are assigned here since the bench exclusively samples pump.fun top tokens." queries: p50: quantile_over_time(0.50, avg(memecoin_platform_fee_pct{platform="pump-fun"})[24h:5m]) p90: quantile_over_time(0.90, avg(memecoin_platform_fee_pct{platform="pump-fun"})[24h:5m]) @@ -78,10 +75,32 @@ providers: mean: avg_over_time(avg(memecoin_platform_fee_pct{platform="pump-fun"})[24h:5m]) sample_size: sum(avg_over_time(memecoin_platform_trade_count{platform="pump-fun"}[24h])) + - slug: phantom + name: Phantom + tag: Wallet + swap UI + formula: "Explicit on-chain fee percent for trades tagged phantom by Mobula, 24h median." + queries: + p50: quantile_over_time(0.50, avg(memecoin_platform_fee_pct{platform="phantom"})[24h:5m]) + p90: quantile_over_time(0.90, avg(memecoin_platform_fee_pct{platform="phantom"})[24h:5m]) + p99: quantile_over_time(0.99, avg(memecoin_platform_fee_pct{platform="phantom"})[24h:5m]) + mean: avg_over_time(avg(memecoin_platform_fee_pct{platform="phantom"})[24h:5m]) + sample_size: sum(avg_over_time(memecoin_platform_trade_count{platform="phantom"}[24h])) + + - slug: fomo + name: Fomo + tag: Mobile app + formula: "Explicit on-chain fee percent for trades tagged fomo by Mobula, 24h median. Fomo collects a fee in USDC transferred to a confirmed fee wallet per trade." + queries: + p50: quantile_over_time(0.50, avg(memecoin_platform_fee_pct{platform="fomo"})[24h:5m]) + p90: quantile_over_time(0.90, avg(memecoin_platform_fee_pct{platform="fomo"})[24h:5m]) + p99: quantile_over_time(0.99, avg(memecoin_platform_fee_pct{platform="fomo"})[24h:5m]) + mean: avg_over_time(avg(memecoin_platform_fee_pct{platform="fomo"})[24h:5m]) + sample_size: sum(avg_over_time(memecoin_platform_trade_count{platform="fomo"}[24h])) + - slug: photon name: Photon tag: Aggregator frontend - formula: "Average total fee percent across top pump.fun tokens traded via Photon, 24h median." + formula: "Explicit on-chain fee percent for trades tagged photon by Mobula, 24h median." queries: p50: quantile_over_time(0.50, avg(memecoin_platform_fee_pct{platform="photon"})[24h:5m]) p90: quantile_over_time(0.90, avg(memecoin_platform_fee_pct{platform="photon"})[24h:5m]) @@ -92,7 +111,7 @@ providers: - slug: axiom name: Axiom tag: Aggregator frontend - formula: "Average total fee percent across top pump.fun tokens traded via Axiom, 24h median." + formula: "Explicit on-chain fee percent for trades tagged axiom by Mobula, 24h median." queries: p50: quantile_over_time(0.50, avg(memecoin_platform_fee_pct{platform="axiom"})[24h:5m]) p90: quantile_over_time(0.90, avg(memecoin_platform_fee_pct{platform="axiom"})[24h:5m]) @@ -103,7 +122,7 @@ providers: - slug: trojan name: Trojan tag: Trading bot - formula: "Average total fee percent across top pump.fun tokens traded via Trojan bot, 24h median." + formula: "Explicit on-chain fee percent for trades tagged trojan by Mobula, 24h median." queries: p50: quantile_over_time(0.50, avg(memecoin_platform_fee_pct{platform="trojan"})[24h:5m]) p90: quantile_over_time(0.90, avg(memecoin_platform_fee_pct{platform="trojan"})[24h:5m]) @@ -114,7 +133,7 @@ providers: - slug: gmgn name: GMGN tag: Trading bot - formula: "Average total fee percent across top pump.fun tokens traded via GMGN, 24h median." + formula: "Explicit on-chain fee percent for trades tagged gmgn by Mobula, 24h median." queries: p50: quantile_over_time(0.50, avg(memecoin_platform_fee_pct{platform="gmgn"})[24h:5m]) p90: quantile_over_time(0.90, avg(memecoin_platform_fee_pct{platform="gmgn"})[24h:5m]) diff --git a/harnesses/evm-exec/internal/platform/platforms.go b/harnesses/evm-exec/internal/platform/platforms.go index 5ffd61f2..a5831686 100644 --- a/harnesses/evm-exec/internal/platform/platforms.go +++ b/harnesses/evm-exec/internal/platform/platforms.go @@ -64,17 +64,38 @@ var PlatformConfig = map[string]map[string]EVMPlatform{ BootstrapDays: 30, }, }, - // FOMO — Gnosis Safe multisig 0x9fc4e320 confirmed via Relay API appFees field (30/37 fee txs) - // Same address on BSC/Base/ETH; EVM-native trades only (Solana→EVM fees collected on Solana side) - "fomo": { + // Maestro — confirmed via DeFiLlama fees/maestro adapter, same address all EVM chains + "maestro": { + "ethereum": { + FeeCollector: "0xb0999731f7c2581844658a9d2ced1be0077b7397", + NativeEnabled: true, + ERC20Tokens: []string{USDC_ETH}, + BootstrapDays: 30, + }, "bsc": { - FeeCollector: "0x9fc4e320a181e88644a302d11f1f158ef0699e37", + FeeCollector: "0xb0999731f7c2581844658a9d2ced1be0077b7397", NativeEnabled: true, ERC20Tokens: []string{USDC_BSC}, BootstrapDays: 30, }, "base": { - FeeCollector: "0x9fc4e320a181e88644a302d11f1f158ef0699e37", + FeeCollector: "0xb0999731f7c2581844658a9d2ced1be0077b7397", + NativeEnabled: false, + ERC20Tokens: []string{USDC_BASE}, + BootstrapDays: 30, + }, + }, + // Banana Gun — bananagun-fees.eth resolves to this address, confirmed via Etherscan/Basescan labels + // BSC fee wallet unconfirmed — skipped until verified + "banana-gun": { + "ethereum": { + FeeCollector: "0x72172c02da4c39d6d05c2b9458501faf671d8ef8", + NativeEnabled: true, + ERC20Tokens: []string{USDC_ETH}, + BootstrapDays: 30, + }, + "base": { + FeeCollector: "0x72172c02da4c39d6d05c2b9458501faf671d8ef8", NativeEnabled: false, ERC20Tokens: []string{USDC_BASE}, BootstrapDays: 30, @@ -100,6 +121,15 @@ var PlatformConfig = map[string]map[string]EVMPlatform{ BootstrapDays: 30, }, }, + // Axiom — confirmed via DeFiLlama fees/axiom adapter (feeReceiver field, start 2026-01-25) + "axiom": { + "bsc": { + FeeCollector: "0xdec29d79e8cdf009d2fa33e0558cb5648481cac3", + NativeEnabled: true, + ERC20Tokens: []string{USDC_BSC}, + BootstrapDays: 30, + }, + }, } // Coverage returns "full" if native is enabled for this platform+chain, else "stable-only". diff --git a/harnesses/memecoin-platforms/cmd/monitor/main.go b/harnesses/memecoin-platforms/cmd/monitor/main.go index 60b07cf1..b44da9a2 100644 --- a/harnesses/memecoin-platforms/cmd/monitor/main.go +++ b/harnesses/memecoin-platforms/cmd/monitor/main.go @@ -9,6 +9,8 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" ) +const minTradeUSD = 5.0 + func main() { log.Println("memecoin-platforms monitor starting...") @@ -17,17 +19,27 @@ func main() { log.Fatal("MOBULA_API_KEY is required") } - interval := 5 * time.Minute - client := &http.Client{Timeout: 30 * time.Second} + mobulaClient := &http.Client{Timeout: 30 * time.Second} + rpcClient := &http.Client{Timeout: 10 * time.Second} - // First poll immediately, then tick. - runPoll(client, apiKey) + setSolPrice(175.0) + updateSolPrice(mobulaClient) go func() { - ticker := time.NewTicker(interval) + ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for range ticker.C { - runPoll(client, apiKey) + updateSolPrice(mobulaClient) + } + }() + + runPoll(mobulaClient, rpcClient, apiKey) + + go func() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + for range ticker.C { + runPoll(mobulaClient, rpcClient, apiKey) } }() @@ -40,15 +52,13 @@ func main() { } type platformStats struct { - platformFeeSum float64 - gasSum float64 - totalFeeSum float64 - tradeValueSum float64 - n int + totalFeeSum float64 + tradeValueSum float64 + n int } -func runPoll(client *http.Client, apiKey string) { - tokens, err := fetchTopTokens(client, 10) +func runPoll(mobulaClient, rpcClient *http.Client, apiKey string) { + tokens, err := fetchTopTokens(mobulaClient, 10) if err != nil { log.Printf("[pump.fun] %v", err) pollErrors.WithLabelValues("pumpfun").Inc() @@ -61,23 +71,21 @@ func runPoll(client *http.Client, apiKey string) { continue } - trades, err := fetchTrades(client, apiKey, tok.Mint) + trades, err := fetchTrades(mobulaClient, 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 { + if t.AmountUSD < minTradeUSD { + continue + } p := t.Platform if p == "" { - // Mobula does not tag pump.fun native trades — they return - // platform:null. Since we only sample pump.fun top tokens, - // untagged trades are overwhelmingly pump.fun bonding curve - // or its graduated Raydium pool. p = "pump-fun" } s := byPlatform[p] @@ -85,9 +93,8 @@ func runPoll(client *http.Client, apiKey string) { s = &platformStats{} byPlatform[p] = s } - s.platformFeeSum += t.PlatformFeesUSD - s.gasSum += t.GasFeesUSD - s.totalFeeSum += t.TotalFeesUSD + feeUSD := computeExplicitFees(rpcClient, t.Hash, t.Sender) + s.totalFeeSum += feeUSD s.tradeValueSum += t.AmountUSD s.n++ } @@ -99,19 +106,13 @@ func runPoll(client *http.Client, apiKey string) { 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) } diff --git a/harnesses/memecoin-platforms/cmd/monitor/metrics.go b/harnesses/memecoin-platforms/cmd/monitor/metrics.go index 9a9b6a14..9677ad5a 100644 --- a/harnesses/memecoin-platforms/cmd/monitor/metrics.go +++ b/harnesses/memecoin-platforms/cmd/monitor/metrics.go @@ -8,27 +8,17 @@ import ( 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", + Help: "Explicit on-chain fee as % of trade value (gas + platform fee wallets), trades >= $5", }, []string{"platform", "token", "token_address"}) platformTotalFeeUSD = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "memecoin_platform_total_fee_usd", - Help: "Average total fee in USD per trade", + Help: "Average explicit on-chain fee in USD per trade (gas + platform fee wallets)", }, []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", + Help: "Number of trades sampled (amountUSD >= 5) for this platform and token", }, []string{"platform", "token", "token_address"}) pollErrors = promauto.NewCounterVec(prometheus.CounterOpts{ diff --git a/harnesses/memecoin-platforms/cmd/monitor/mobula.go b/harnesses/memecoin-platforms/cmd/monitor/mobula.go index 7ef41453..5384388d 100644 --- a/harnesses/memecoin-platforms/cmd/monitor/mobula.go +++ b/harnesses/memecoin-platforms/cmd/monitor/mobula.go @@ -8,12 +8,9 @@ import ( ) 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 %. + Platform string `json:"platform"` + Hash string `json:"hash"` + Sender string `json:"sender"` AmountUSD float64 `json:"amountUSD"` } diff --git a/harnesses/memecoin-platforms/cmd/monitor/pumpfun.go b/harnesses/memecoin-platforms/cmd/monitor/pumpfun.go index f1d35926..60f7cd46 100644 --- a/harnesses/memecoin-platforms/cmd/monitor/pumpfun.go +++ b/harnesses/memecoin-platforms/cmd/monitor/pumpfun.go @@ -8,10 +8,11 @@ import ( ) type PumpToken struct { - Mint string `json:"mint"` - Symbol string `json:"symbol"` - Name string `json:"name"` - MCap float64 `json:"market_cap"` + Mint string `json:"mint"` + Symbol string `json:"symbol"` + Name string `json:"name"` + MCap float64 `json:"market_cap"` + Complete bool `json:"complete"` } func fetchTopTokens(client *http.Client, limit int) ([]PumpToken, error) { diff --git a/harnesses/memecoin-platforms/cmd/monitor/solana.go b/harnesses/memecoin-platforms/cmd/monitor/solana.go new file mode 100644 index 00000000..8a93c51a --- /dev/null +++ b/harnesses/memecoin-platforms/cmd/monitor/solana.go @@ -0,0 +1,297 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "math" + "net/http" + "sync" + "sync/atomic" + "time" +) + +const ( + solanaRPC = "https://api.mainnet-beta.solana.com" + usdcMint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + wsolMint = "So11111111111111111111111111111111111111112" + rpcSleep = 40 * time.Millisecond +) + +// Known fee wallet owners confirmed via on-chain analysis of tagged Mobula trades. +// Key = owner address of the token account receiving the fee. +var platformFeeOwners = map[string]string{ + "R4rNJHaffSUotNmqSKNEfDcJE8A7zJUkaoM5Jkd7cYX": "fomo", +} + +// Program/system accounts that should never be treated as fee recipients. +var skipAccounts = map[string]bool{ + "11111111111111111111111111111111": true, + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA": true, + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb": true, + "ComputeBudget111111111111111111111111111111": true, + "SysvarC1ock11111111111111111111111111111111": true, + "Sysvar1nstructions1111111111111111111111111": true, + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL": true, + "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr": true, + "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo": true, + "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK": true, + "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C": true, + "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8": true, +} + +var ( + txCache sync.Map + solPriceAtom uint64 +) + +func setSolPrice(p float64) { atomic.StoreUint64(&solPriceAtom, math.Float64bits(p)) } +func getSolPrice() float64 { return math.Float64frombits(atomic.LoadUint64(&solPriceAtom)) } + +func updateSolPrice(client *http.Client) { + req, _ := http.NewRequest("GET", + "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd", nil) + req.Header.Set("User-Agent", "OCBBot/1.0") + resp, err := client.Do(req) + if err != nil { + log.Printf("[solprice] %v", err) + return + } + defer resp.Body.Close() + var data map[string]map[string]float64 + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return + } + if p, ok := data["solana"]["usd"]; ok && p > 0 { + setSolPrice(p) + log.Printf("[solprice] %.2f USD", p) + } +} + +type solanaTxResp struct { + Result *struct { + Meta struct { + Fee int64 `json:"fee"` + PreBalances []int64 `json:"preBalances"` + PostBalances []int64 `json:"postBalances"` + PreTokenBalances []tokBal `json:"preTokenBalances"` + PostTokenBalances []tokBal `json:"postTokenBalances"` + } `json:"meta"` + Transaction struct { + Message struct { + AccountKeys []json.RawMessage `json:"accountKeys"` + } `json:"message"` + } `json:"transaction"` + } `json:"result"` +} + +type tokBal struct { + AccountIndex int `json:"accountIndex"` + Mint string `json:"mint"` + Owner string `json:"owner"` + UITokenAmount struct { + UIAmount *float64 `json:"uiAmount"` + } `json:"uiTokenAmount"` +} + +// computeExplicitFees returns the explicit on-chain fee in USD for a trade tx: +// gas + known platform fee wallet receipts + heuristic small SOL/USDC recipients. +// AMM LP fees that stay inside pool accounts are not counted. +// Returns 0 on RPC error (logs the failure). +func computeExplicitFees(rpcClient *http.Client, txHash, sender string) float64 { + if txHash == "" { + return 0 + } + if v, ok := txCache.Load(txHash); ok { + return v.(float64) + } + fee, err := fetchOnChainFee(rpcClient, txHash, sender) + if err != nil { + log.Printf("[solana] %s: %v", txHash[:min(12, len(txHash))], err) + txCache.Store(txHash, float64(0)) + return 0 + } + txCache.Store(txHash, fee) + time.Sleep(rpcSleep) + return fee +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func fetchOnChainFee(rpcClient *http.Client, txHash, sender string) (float64, error) { + body, _ := json.Marshal(map[string]interface{}{ + "jsonrpc": "2.0", "id": 1, + "method": "getTransaction", + "params": []interface{}{txHash, map[string]interface{}{ + "encoding": "jsonParsed", + "maxSupportedTransactionVersion": 0, + }}, + }) + req, _ := http.NewRequest("POST", solanaRPC, bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + + resp, err := rpcClient.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + var out solanaTxResp + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return 0, fmt.Errorf("decode: %w", err) + } + if out.Result == nil { + return 0, fmt.Errorf("not found") + } + + meta := out.Result.Meta + msg := out.Result.Transaction.Message + solPrice := getSolPrice() + + // Parse account keys (string or {pubkey,...} objects). + var accounts []string + for _, raw := range msg.AccountKeys { + var s string + if json.Unmarshal(raw, &s) == nil { + accounts = append(accounts, s) + continue + } + var obj struct { + Pubkey string `json:"pubkey"` + } + if json.Unmarshal(raw, &obj) == nil { + accounts = append(accounts, obj.Pubkey) + } + } + + gasUSD := float64(meta.Fee) / 1e9 * solPrice + + // Token balance deltas by (owner, mint). + type ownerMint struct{ owner, mint string } + tokDelta := map[ownerMint]float64{} + preTok := map[int]tokBal{} + postTok := map[int]tokBal{} + for _, t := range meta.PreTokenBalances { preTok[t.AccountIndex] = t } + for _, t := range meta.PostTokenBalances { postTok[t.AccountIndex] = t } + + for idx := range accounts { + p, hasPre := preTok[idx] + q, hasPost := postTok[idx] + if !hasPre && !hasPost { + continue + } + var mint, owner string + var preAmt, postAmt float64 + if hasPost { + mint = q.Mint + owner = q.Owner + if q.UITokenAmount.UIAmount != nil { + postAmt = *q.UITokenAmount.UIAmount + } + } + if hasPre { + if mint == "" { + mint = p.Mint + owner = p.Owner + } + if p.UITokenAmount.UIAmount != nil { + preAmt = *p.UITokenAmount.UIAmount + } + } + tokDelta[ownerMint{owner, mint}] += postAmt - preAmt + } + + // 1. Known platform fee wallet owners (exact, confirmed on-chain). + var knownFeeUSD float64 + for key, delta := range tokDelta { + if _, ok := platformFeeOwners[key.owner]; !ok || delta <= 0 { + continue + } + switch key.mint { + case usdcMint: + knownFeeUSD += delta + case wsolMint: + knownFeeUSD += delta * solPrice + } + } + + // 2. Small SOL recipients heuristic (bot fees / referral tips in native SOL). + // Anything receiving < 5% of the user's net SOL flow that isn't a system account. + userIdx := -1 + for i, acc := range accounts { + if acc == sender { + userIdx = i + break + } + } + userAbsFlow := int64(0) + if userIdx >= 0 && userIdx < len(meta.PreBalances) { + d := meta.PostBalances[userIdx] - meta.PreBalances[userIdx] + if d < 0 { + userAbsFlow = -d + } else { + userAbsFlow = d + } + } + + var smallSolFeeUSD float64 + for i, acc := range accounts { + if i == userIdx || skipAccounts[acc] { + continue + } + if _, ok := platformFeeOwners[acc]; ok { + continue + } + d := meta.PostBalances[i] - meta.PreBalances[i] + if d <= 0 { + continue + } + if userAbsFlow > 0 && d < userAbsFlow/20 { + smallSolFeeUSD += float64(d) / 1e9 * solPrice + } + } + + // 3. Small USDC/WSOL token recipients on the input side (Fomo-style referral fees). + var smallTokFeeUSD float64 + for key, delta := range tokDelta { + if key.owner != sender || delta >= 0 { + continue + } + if key.mint != usdcMint && key.mint != wsolMint { + continue + } + maxRecv := 0.0 + for k2, d2 := range tokDelta { + if k2.mint == key.mint && d2 > maxRecv && !skipAccounts[k2.owner] { + maxRecv = d2 + } + } + for k2, d2 := range tokDelta { + if k2.mint != key.mint || d2 <= 0 || k2.owner == sender { + continue + } + if skipAccounts[k2.owner] { + continue + } + if _, ok := platformFeeOwners[k2.owner]; ok { + continue + } + if maxRecv > 0 && d2 < maxRecv*0.05 { + switch key.mint { + case usdcMint: + smallTokFeeUSD += d2 + case wsolMint: + smallTokFeeUSD += d2 * solPrice + } + } + } + } + + return gasUSD + knownFeeUSD + smallSolFeeUSD + smallTokFeeUSD, nil +} diff --git a/public/logos/dex-screener.png b/public/logos/dex-screener.png new file mode 100644 index 00000000..a86efa35 Binary files /dev/null and b/public/logos/dex-screener.png differ diff --git a/public/logos/moonshot.svg b/public/logos/moonshot.svg new file mode 100644 index 00000000..6e67b8ab --- /dev/null +++ b/public/logos/moonshot.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/logos/phantom.svg b/public/logos/phantom.svg new file mode 100644 index 00000000..9cc90aa9 --- /dev/null +++ b/public/logos/phantom.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/public/logos/pumpfun.svg b/public/logos/pumpfun.svg new file mode 100644 index 00000000..c264f37f --- /dev/null +++ b/public/logos/pumpfun.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/app/apps/exec/page.tsx b/src/app/apps/exec/page.tsx index 02ef9486..d8537c7e 100644 --- a/src/app/apps/exec/page.tsx +++ b/src/app/apps/exec/page.tsx @@ -72,7 +72,7 @@ export default async function ExecPage() { the 8 official Jito tip accounts. Higher = more MEV-sensitive routing.

- Platform fee = average SOL transferred to the platform's fee account + Platform fee = average SOL transferred to the platform's fee account per transaction.

diff --git a/src/lib/brand.ts b/src/lib/brand.ts index bcc2968a..01e9f993 100644 --- a/src/lib/brand.ts +++ b/src/lib/brand.ts @@ -77,6 +77,13 @@ const BRANDS: Record = { relay: { color: "#26D49B" }, // mint across: { color: "#6CF9D8" }, // across aqua + // ─── Crypto trading apps (bench № 202) ─── + pumpfun: { color: "#00C851" }, // pump.fun green + phantom: { color: "#AB9FF2" }, // phantom purple + moonshot: { color: "#F97316" }, // moonshot orange + "dex-screener": { color: "#1C1C1C", dark: true }, // dex screener dark + fomo: { color: "#7C3AED" }, // fomo violet + // ─── Trading / perps ─── hyperliquid: { color: "#22D3EE" }, // cyan dydx: { color: "#C084FC" }, // bright violet diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index 3174092f..4a2de723 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -297,6 +297,12 @@ const RAW: Record = { trojan: "/logos/trojan.jpg", gmgn: "/logos/gmgn.jpg", maestro: "/logos/maestro.jpg", + + // ─── App Store ratings (bench № 202) ─── + pumpfun: "/logos/pumpfun.svg", + phantom: "/logos/phantom.svg", + moonshot: "/logos/moonshot.svg", + "dex-screener": "/logos/dex-screener.png", // ─── Hyperliquid frontends (bench № 030) ─── "phantom-perps": "/logos/phantom-perps.svg", axiom: "/logos/axiom.png",