diff --git a/harnesses/memecoin-platforms/cmd/monitor/main.go b/harnesses/memecoin-platforms/cmd/monitor/main.go index 5142b5ec..b19ac938 100644 --- a/harnesses/memecoin-platforms/cmd/monitor/main.go +++ b/harnesses/memecoin-platforms/cmd/monitor/main.go @@ -3,6 +3,7 @@ package main import ( "log" "net/http" + "net/url" "os" "time" @@ -19,8 +20,27 @@ func main() { log.Fatal("MOBULA_API_KEY is required") } + heliusKey := os.Getenv("HELIUS_API_KEY") + mobulaClient := &http.Client{Timeout: 30 * time.Second} - rpcClient := &http.Client{Timeout: 10 * time.Second} + + // rpcClient routes through rotating proxy to avoid per-IP rate limits + var rpcClient *http.Client + if proxyRaw := os.Getenv("HTTPS_PROXY"); proxyRaw != "" { + if proxyURL, err := url.Parse(proxyRaw); err == nil { + rpcClient = &http.Client{ + Timeout: 15 * time.Second, + Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}, + } + log.Printf("rpc: rotating proxy enabled") + } + } + if rpcClient == nil { + rpcClient = &http.Client{Timeout: 15 * time.Second} + } + + // heliusClient used as fallback for older tx not in public RPC history + heliusClient := &http.Client{Timeout: 15 * time.Second} setSolPrice(175.0) updateSolPrice(mobulaClient) @@ -42,12 +62,12 @@ func main() { log.Fatal(http.ListenAndServe(":9090", nil)) }() - runPoll(mobulaClient, rpcClient, apiKey) + runPoll(mobulaClient, rpcClient, heliusClient, heliusKey, apiKey) ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for range ticker.C { - runPoll(mobulaClient, rpcClient, apiKey) + runPoll(mobulaClient, rpcClient, heliusClient, heliusKey, apiKey) } } @@ -57,7 +77,7 @@ type platformStats struct { n int } -func runPoll(mobulaClient, rpcClient *http.Client, apiKey string) { +func runPoll(mobulaClient, rpcClient, heliusClient *http.Client, heliusKey, apiKey string) { tokens, err := fetchTopTokens(mobulaClient, 10) if err != nil { log.Printf("[pump.fun] %v", err) @@ -95,7 +115,7 @@ func runPoll(mobulaClient, rpcClient *http.Client, apiKey string) { byPlatform[p] = s } _, cached := txCache.Load(t.Hash) - if !cached && freshLookups >= 15 { + if !cached && freshLookups >= 20 { s.tradeValueSum += t.AmountUSD s.n++ continue @@ -103,7 +123,7 @@ func runPoll(mobulaClient, rpcClient *http.Client, apiKey string) { if !cached { freshLookups++ } - feeUSD := computeExplicitFees(rpcClient, t.Hash, t.Sender) + feeUSD := computeExplicitFees(rpcClient, heliusClient, heliusKey, t.Hash, t.Sender) s.totalFeeSum += feeUSD s.tradeValueSum += t.AmountUSD s.n++ diff --git a/harnesses/memecoin-platforms/cmd/monitor/mobula.go b/harnesses/memecoin-platforms/cmd/monitor/mobula.go index 3a65f19c..135b7aab 100644 --- a/harnesses/memecoin-platforms/cmd/monitor/mobula.go +++ b/harnesses/memecoin-platforms/cmd/monitor/mobula.go @@ -20,7 +20,7 @@ type mobulaTradesResp struct { } func fetchTrades(client *http.Client, apiKey, mint string) ([]MobulaTrade, error) { - from := time.Now().Add(-10 * time.Minute).UnixMilli() + from := time.Now().Add(-2 * time.Hour).UnixMilli() url := fmt.Sprintf( "https://api.mobula.io/api/2/token/trades-enriched?address=%s&chainId=solana:solana&sortOrder=desc&limit=50&from=%d", mint, from, diff --git a/harnesses/memecoin-platforms/cmd/monitor/solana.go b/harnesses/memecoin-platforms/cmd/monitor/solana.go index 9d9091b8..252301d8 100644 --- a/harnesses/memecoin-platforms/cmd/monitor/solana.go +++ b/harnesses/memecoin-platforms/cmd/monitor/solana.go @@ -13,10 +13,11 @@ import ( ) const ( - solanaRPC = "https://solana-rpc.publicnode.com" + rpcPublic = "https://api.mainnet-beta.solana.com" + rpcHelius = "https://mainnet.helius-rpc.com/?api-key=" usdcMint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" wsolMint = "So11111111111111111111111111111111111111112" - rpcSleep = 1 * time.Second + rpcSleep = 200 * time.Millisecond ) // Known fee wallet owners confirmed via on-chain analysis of tagged Mobula trades. @@ -104,7 +105,7 @@ type tokBal struct { // 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 { +func computeExplicitFees(proxyClient, heliusClient *http.Client, heliusKey, txHash, sender string) float64 { if txHash == "" { return 0 } @@ -115,7 +116,10 @@ func computeExplicitFees(rpcClient *http.Client, txHash, sender string) float64 txCache.Range(func(k, _ any) bool { txCache.Delete(k); return true }) txCacheSize.Store(0) } - fee, err := fetchOnChainFee(rpcClient, txHash, sender) + fee, err := fetchOnChainFee(proxyClient, rpcPublic, txHash, sender) + if err != nil && heliusKey != "" { + fee, err = fetchOnChainFee(heliusClient, rpcHelius+heliusKey, txHash, sender) + } if err != nil { if err.Error() != "not found" { log.Printf("[solana] %s: %v", txHash[:min(12, len(txHash))], err) @@ -130,7 +134,7 @@ func computeExplicitFees(rpcClient *http.Client, txHash, sender string) float64 return fee } -func fetchOnChainFee(rpcClient *http.Client, txHash, sender string) (float64, error) { +func fetchOnChainFee(rpcClient *http.Client, rpcURL, txHash, sender string) (float64, error) { body, _ := json.Marshal(map[string]interface{}{ "jsonrpc": "2.0", "id": 1, "method": "getTransaction", @@ -139,7 +143,7 @@ func fetchOnChainFee(rpcClient *http.Client, txHash, sender string) (float64, er "maxSupportedTransactionVersion": 0, }}, }) - req, _ := http.NewRequest("POST", solanaRPC, bytes.NewReader(body)) + req, _ := http.NewRequest("POST", rpcURL, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, err := rpcClient.Do(req) diff --git a/src/app/apps/page.tsx b/src/app/apps/page.tsx index 4f12a426..c849bc20 100644 --- a/src/app/apps/page.tsx +++ b/src/app/apps/page.tsx @@ -2,7 +2,6 @@ import type { Metadata } from "next"; import { pageMetadata } from "@/lib/page-metadata"; import { safeJsonLd, buildBreadcrumbJsonLd } from "@/lib/jsonld"; import { SITE } from "@/data/site"; -import { fetchAppsLeaderboard } from "@/lib/apps-leaderboard"; import { fetchEVMRevenue } from "@/lib/evm-exec"; import { fetchExecLeaderboard } from "@/lib/solana-exec"; import { fetchSolPrice } from "@/lib/sol-price"; @@ -11,19 +10,18 @@ import { TradingAppsLeaderboard, type UnifiedAppRow } from "@/components/trading import Link from "next/link"; const DESCRIPTION = - "Protocol fees collected by trading apps: meme bots, telegram bots, and perps. On-chain data, updated hourly."; + "Protocol fees collected by trading apps: meme bots and telegram bots. On-chain data, updated hourly."; export const metadata: Metadata = pageMetadata({ path: "/apps", - title: "Trading App Revenue — pump.fun, Axiom, GMGN, Hyperliquid | OpenChainBench", + title: "Trading App Revenue — pump.fun, Axiom, GMGN, BullX | OpenChainBench", description: DESCRIPTION, }); export const revalidate = 300; export default async function AppsHubPage() { - const [perpsData, evmData, solanaData, solPrice] = await Promise.all([ - fetchAppsLeaderboard(), + const [evmData, solanaData, solPrice] = await Promise.all([ fetchEVMRevenue(), fetchExecLeaderboard(), fetchSolPrice(), @@ -37,22 +35,7 @@ export default async function AppsHubPage() { (solanaData?.platforms ?? []).map((p) => [p.platform, p]) ); - const perpsBySlug = new Map( - (perpsData?.protocols ?? []).map((p) => [p.slug, p]) - ); - const rows: UnifiedAppRow[] = TRADING_APPS.map((meta) => { - if (meta.perpsSlug !== null) { - const perp = perpsBySlug.get(meta.perpsSlug); - const gross = perp?.windows["24h"]?.gross ?? 0; - return { - meta, - fees: { solana: null, ethereum: null, bsc: null, base: null }, - stableOnly: { ethereum: false, bsc: false, base: false }, - total24h: gross, - }; - } - const evmRow = meta.evmKey ? evmByPlatform.get(meta.evmKey) : undefined; const solRow = meta.solanaKey ? solanaByPlatform.get(meta.solanaKey) : undefined; @@ -87,8 +70,7 @@ export default async function AppsHubPage() { }; }); - const updatedAt = - evmData?.updatedAt ?? solanaData?.updatedAt ?? perpsData?.updatedAt ?? null; + const updatedAt = evmData?.updatedAt ?? solanaData?.updatedAt ?? null; const breadcrumb = { "@context": "https://schema.org", @@ -119,8 +101,7 @@ export default async function AppsHubPage() {

Solana fees = txCount × avgPlatformFeeLamports / 1e9 × SOL price. - EVM fees = stable (USDC/USDT) + native where traceable. - Perps = gross fees 24h from on-chain data.{" "} + EVM fees = stable (USDC/USDT) + native where traceable.{" "} Detailed execution metrics → diff --git a/src/components/trading-apps-leaderboard.tsx b/src/components/trading-apps-leaderboard.tsx index d77f55c8..ea80b9cf 100644 --- a/src/components/trading-apps-leaderboard.tsx +++ b/src/components/trading-apps-leaderboard.tsx @@ -17,25 +17,22 @@ export type UnifiedAppRow = { total24h: number; }; -type TabKey = "all" | "meme-bot" | "telegram-bot" | "perps"; +type TabKey = "all" | "meme-bot" | "telegram-bot"; const TABS: { key: TabKey; label: string }[] = [ { key: "all", label: "All" }, { key: "meme-bot", label: "Meme Bots" }, { key: "telegram-bot", label: "Telegram Bots" }, - { key: "perps", label: "Perps" }, ]; const CATEGORY_BADGE: Record = { "meme-bot": "bg-orange-500/10 text-orange-400 border border-orange-500/20", "telegram-bot": "bg-blue-500/10 text-blue-400 border border-blue-500/20", - perps: "bg-purple-500/10 text-purple-400 border border-purple-500/20", }; const CATEGORY_LABEL: Record = { "meme-bot": "Meme Bot", "telegram-bot": "Telegram Bot", - perps: "Perps", }; function fmtUSD(n: number | null): string { diff --git a/src/lib/trading-apps-config.ts b/src/lib/trading-apps-config.ts index f5116f8c..65d07c44 100644 --- a/src/lib/trading-apps-config.ts +++ b/src/lib/trading-apps-config.ts @@ -1,30 +1,22 @@ export type AppMeta = { id: string; name: string; - category: "meme-bot" | "telegram-bot" | "perps"; + category: "meme-bot" | "telegram-bot"; logoKey: string | null; productUrl: string; benchUrl: string | null; evmKey: string | null; solanaKey: string | null; - perpsSlug: string | null; }; export const TRADING_APPS: AppMeta[] = [ - { id: "pump.fun", name: "pump.fun", category: "meme-bot", logoKey: "pump-fun", productUrl: "https://pump.fun", benchUrl: null, evmKey: "pumpfun", solanaKey: "pump.fun", perpsSlug: null }, - { id: "fomo", name: "FOMO", category: "meme-bot", logoKey: "fomo", productUrl: "https://fomo.fund", benchUrl: null, evmKey: null, solanaKey: "fomo", perpsSlug: null }, - { id: "bullx", name: "BullX", category: "meme-bot", logoKey: "bullx", productUrl: "https://bullx.io", benchUrl: null, evmKey: null, solanaKey: "bullx", perpsSlug: null }, - { id: "photon", name: "Photon", category: "meme-bot", logoKey: "photon", productUrl: "https://photon-sol.tinyastro.io", benchUrl: null, evmKey: null, solanaKey: "photon", perpsSlug: null }, - { id: "gmgn", name: "GMGN", category: "telegram-bot", logoKey: "gmgn", productUrl: "https://gmgn.ai", benchUrl: null, evmKey: "gmgn", solanaKey: "gmgn", perpsSlug: null }, - { id: "axiom", name: "Axiom", category: "telegram-bot", logoKey: "axiom", productUrl: "https://axiom.trade", benchUrl: null, evmKey: "axiom", solanaKey: "axiom", perpsSlug: null }, - { id: "maestro", name: "Maestro", category: "telegram-bot", logoKey: "maestro", productUrl: "https://maestro.bots.gg", benchUrl: null, evmKey: "maestro", solanaKey: null, perpsSlug: null }, - { id: "banana-gun", name: "Banana Gun", category: "telegram-bot", logoKey: "banana-gun", productUrl: "https://t.me/BananaGunSniper_bot", benchUrl: null, evmKey: "banana-gun", solanaKey: null, perpsSlug: null }, - { id: "trojan", name: "Trojan", category: "telegram-bot", logoKey: "trojan", productUrl: "https://trojan.bot", benchUrl: null, evmKey: null, solanaKey: "trojan", perpsSlug: null }, - { id: "hyperliquid", name: "Hyperliquid", category: "perps", logoKey: "hyperliquid", productUrl: "https://hyperliquid.xyz", benchUrl: "/hyperliquid", evmKey: null, solanaKey: null, perpsSlug: "hyperliquid" }, - { id: "jupiter-perps", name: "Jupiter Perps", category: "perps", logoKey: "jupiter", productUrl: "https://jup.ag/perps", benchUrl: null, evmKey: null, solanaKey: null, perpsSlug: "jupiter-perps" }, - { id: "gmx", name: "GMX", category: "perps", logoKey: "gmx", productUrl: "https://gmx.io", benchUrl: null, evmKey: null, solanaKey: null, perpsSlug: "gmx" }, - { id: "dydx", name: "dYdX", category: "perps", logoKey: "dydx", productUrl: "https://dydx.exchange", benchUrl: null, evmKey: null, solanaKey: null, perpsSlug: "dydx" }, - { id: "gains-trade", name: "Gains.trade", category: "perps", logoKey: "gains", productUrl: "https://gains.trade", benchUrl: null, evmKey: null, solanaKey: null, perpsSlug: "gains-trade" }, - { id: "drift", name: "Drift", category: "perps", logoKey: "drift", productUrl: "https://drift.trade", benchUrl: null, evmKey: null, solanaKey: null, perpsSlug: "drift" }, - { id: "vertex", name: "Vertex", category: "perps", logoKey: "vertex", productUrl: "https://vertexprotocol.com", benchUrl: null, evmKey: null, solanaKey: null, perpsSlug: "vertex" }, + { id: "pump.fun", name: "pump.fun", category: "meme-bot", logoKey: "pump-fun", productUrl: "https://pump.fun", benchUrl: null, evmKey: "pumpfun", solanaKey: "pump.fun" }, + { id: "fomo", name: "FOMO", category: "meme-bot", logoKey: "fomo", productUrl: "https://fomo.fund", benchUrl: null, evmKey: null, solanaKey: "fomo" }, + { id: "bullx", name: "BullX", category: "meme-bot", logoKey: "bullx", productUrl: "https://bullx.io", benchUrl: null, evmKey: null, solanaKey: "bullx" }, + { id: "photon", name: "Photon", category: "meme-bot", logoKey: "photon", productUrl: "https://photon-sol.tinyastro.io", benchUrl: null, evmKey: null, solanaKey: "photon" }, + { id: "gmgn", name: "GMGN", category: "telegram-bot", logoKey: "gmgn", productUrl: "https://gmgn.ai", benchUrl: null, evmKey: "gmgn", solanaKey: "gmgn" }, + { id: "axiom", name: "Axiom", category: "telegram-bot", logoKey: "axiom", productUrl: "https://axiom.trade", benchUrl: null, evmKey: "axiom", solanaKey: "axiom" }, + { id: "maestro", name: "Maestro", category: "telegram-bot", logoKey: "maestro", productUrl: "https://maestro.bots.gg", benchUrl: null, evmKey: "maestro", solanaKey: null }, + { id: "banana-gun", name: "Banana Gun", category: "telegram-bot", logoKey: "banana-gun", productUrl: "https://t.me/BananaGunSniper_bot", benchUrl: null, evmKey: "banana-gun", solanaKey: null }, + { id: "trojan", name: "Trojan", category: "telegram-bot", logoKey: "trojan", productUrl: "https://trojan.bot", benchUrl: null, evmKey: null, solanaKey: "trojan" }, ];