diff --git a/harnesses/evm-exec/cmd/collector/main.go b/harnesses/evm-exec/cmd/collector/main.go index 188c68cc..b1ad5ba2 100644 --- a/harnesses/evm-exec/cmd/collector/main.go +++ b/harnesses/evm-exec/cmd/collector/main.go @@ -94,9 +94,16 @@ func collectPlatform(ctx context.Context, db *store.DB, etherscanKey, plt, chain if cfg.NativeEnabled { switch chain { case "ethereum": - if err := collectNativeETH(ctx, db, etherscanKey, plt, chain, cfg.FeeCollector, toBlock); err != nil { + if err := collectNativeEtherscan(ctx, db, etherscanKey, source.ChainIDEthereum, plt, chain, cfg.FeeCollector, toBlock); err != nil { log.Printf("collector: %s/%s native ETH: %v", plt, chain, err) } + case "base": + basescanKey := os.Getenv("BASESCAN_API_KEY") + if basescanKey != "" { + if err := collectNativeEtherscan(ctx, db, basescanKey, source.ChainIDBase, plt, chain, cfg.FeeCollector, toBlock); err != nil { + log.Printf("collector: %s/%s native ETH on Base: %v", plt, chain, err) + } + } case "bsc": if err := collectNativeBSC(ctx, db, plt, chain, cfg.FeeCollector, toBlock); err != nil { log.Printf("collector: %s/%s native BNB: %v", plt, chain, err) @@ -152,17 +159,17 @@ func collectERC20(ctx context.Context, db *store.DB, rpc, plt, chain, collector, return nil } -func collectNativeETH(ctx context.Context, db *store.DB, apiKey, plt, chain, collector string, toBlock uint64) error { +func collectNativeEtherscan(ctx context.Context, db *store.DB, apiKey, chainID, plt, chain, collector string, toBlock uint64) error { cursor, _, err := db.GetCursor(ctx, chain, plt, "native") if err != nil { return fmt.Errorf("get cursor: %w", err) } - internalTxs, lastInt, err := source.GetEtherscanInternalTxs(ctx, apiKey, collector, cursor) + internalTxs, lastInt, err := source.GetEtherscanInternalTxs(ctx, apiKey, chainID, collector, cursor) if err != nil { return err } - normalTxs, lastNorm, err := source.GetEtherscanNormalTxs(ctx, apiKey, collector, cursor) + normalTxs, lastNorm, err := source.GetEtherscanNormalTxs(ctx, apiKey, chainID, collector, cursor) if err != nil { return err } diff --git a/harnesses/evm-exec/internal/platform/platforms.go b/harnesses/evm-exec/internal/platform/platforms.go index 215f0e3e..8f2108c3 100644 --- a/harnesses/evm-exec/internal/platform/platforms.go +++ b/harnesses/evm-exec/internal/platform/platforms.go @@ -59,7 +59,7 @@ var PlatformConfig = map[string]map[string]EVMPlatform{ }, "base": { FeeCollector: "0x16388de42c5829fd0e88c8eb001ef43bfc93f177", - NativeEnabled: false, // no free trace API on Base; USDC only + NativeEnabled: true, // via BaseScan API (gated on BASESCAN_API_KEY env) ERC20Tokens: []string{USDC_BASE}, BootstrapDays: 30, }, @@ -80,7 +80,7 @@ var PlatformConfig = map[string]map[string]EVMPlatform{ }, "base": { FeeCollector: "0xb0999731f7c2581844658a9d2ced1be0077b7397", - NativeEnabled: false, + NativeEnabled: true, // Etherscan V2 chainid=8453 ERC20Tokens: []string{USDC_BASE}, BootstrapDays: 30, }, @@ -101,7 +101,7 @@ var PlatformConfig = map[string]map[string]EVMPlatform{ }, "base": { FeeCollector: "0x1fba6b0bbae2b74586fba407fb45bd4788b7b130", - NativeEnabled: false, + NativeEnabled: true, // Etherscan V2 chainid=8453 ERC20Tokens: []string{USDC_BASE}, BootstrapDays: 30, }, @@ -121,7 +121,7 @@ var PlatformConfig = map[string]map[string]EVMPlatform{ }, "base": { FeeCollector: "0xb8159ba378904f803639d274cec79f788931c9c8", - NativeEnabled: false, // no free trace API; USDC only + NativeEnabled: true, // Etherscan V2 chainid=8453 ERC20Tokens: []string{USDC_BASE}, BootstrapDays: 30, }, diff --git a/harnesses/evm-exec/internal/source/etherscan.go b/harnesses/evm-exec/internal/source/etherscan.go index feccacbe..78843e10 100644 --- a/harnesses/evm-exec/internal/source/etherscan.go +++ b/harnesses/evm-exec/internal/source/etherscan.go @@ -11,7 +11,12 @@ import ( "time" ) -const etherscanV2 = "https://api.etherscan.io/v2/api" +const ( + etherscanV2 = "https://api.etherscan.io/v2/api" + basescanAPI = "https://api.basescan.org/api" + ChainIDEthereum = "1" + ChainIDBase = "" // BaseScan is single-chain; no chainid param needed +) // NativeTx is an ETH transfer (internal or normal) to a monitored address. type NativeTx struct { @@ -33,8 +38,8 @@ type etherscanTx struct { } // GetEtherscanInternalTxs fetches ETH internal transfers to address starting from startBlock. -func GetEtherscanInternalTxs(ctx context.Context, apiKey, address string, startBlock uint64) ([]NativeTx, uint64, error) { - return fetchEtherscanTxs(ctx, apiKey, address, startBlock, "txlistinternal", func(tx etherscanTx) (NativeTx, bool) { +func GetEtherscanInternalTxs(ctx context.Context, apiKey, chainID, address string, startBlock uint64) ([]NativeTx, uint64, error) { + return fetchEtherscanTxs(ctx, apiKey, chainID, address, startBlock, "txlistinternal", func(tx etherscanTx) (NativeTx, bool) { if tx.IsError == "1" || tx.Value == "0" || tx.Value == "" { return NativeTx{}, false } @@ -55,9 +60,9 @@ func GetEtherscanInternalTxs(ctx context.Context, apiKey, address string, startB } // GetEtherscanNormalTxs fetches plain ETH value transfers to address starting from startBlock. -func GetEtherscanNormalTxs(ctx context.Context, apiKey, address string, startBlock uint64) ([]NativeTx, uint64, error) { +func GetEtherscanNormalTxs(ctx context.Context, apiKey, chainID, address string, startBlock uint64) ([]NativeTx, uint64, error) { addrLower := strings.ToLower(address) - return fetchEtherscanTxs(ctx, apiKey, address, startBlock, "txlist", func(tx etherscanTx) (NativeTx, bool) { + return fetchEtherscanTxs(ctx, apiKey, chainID, address, startBlock, "txlist", func(tx etherscanTx) (NativeTx, bool) { if tx.IsError == "1" || tx.Value == "0" || tx.Value == "" { return NativeTx{}, false } @@ -83,7 +88,7 @@ func GetEtherscanNormalTxs(ctx context.Context, apiKey, address string, startBlo type filterFn func(etherscanTx) (NativeTx, bool) // fetchEtherscanTxs handles Etherscan pagination including the 10k-result cap. -func fetchEtherscanTxs(ctx context.Context, apiKey, address string, startBlock uint64, action string, filter filterFn) ([]NativeTx, uint64, error) { +func fetchEtherscanTxs(ctx context.Context, apiKey, chainID, address string, startBlock uint64, action string, filter filterFn) ([]NativeTx, uint64, error) { const ( offset = 1000 maxPage = 10 @@ -102,7 +107,7 @@ func fetchEtherscanTxs(ctx context.Context, apiKey, address string, startBlock u for page <= maxPage { time.Sleep(250 * time.Millisecond) - batch, err := etherscanPage(ctx, apiKey, address, action, curStart, endBlock, page, offset) + batch, err := etherscanPage(ctx, apiKey, chainID, address, action, curStart, endBlock, page, offset) if err != nil { return all, highestBlock, fmt.Errorf("etherscan %s page %d: %w", action, page, err) } @@ -148,9 +153,12 @@ func fetchEtherscanTxs(ctx context.Context, apiKey, address string, startBlock u return all, highestBlock, nil } -func etherscanPage(ctx context.Context, apiKey, address, action string, startBlock uint64, endBlock, page, offset int) ([]etherscanTx, error) { +func etherscanPage(ctx context.Context, apiKey, chainID, address, action string, startBlock uint64, endBlock, page, offset int) ([]etherscanTx, error) { + apiURL := etherscanV2 + if chainID == "" { + apiURL = basescanAPI + } params := url.Values{ - "chainid": {"1"}, "module": {"account"}, "action": {action}, "address": {address}, @@ -161,8 +169,11 @@ func etherscanPage(ctx context.Context, apiKey, address, action string, startBlo "sort": {"asc"}, "apikey": {apiKey}, } + if chainID != "" { + params.Set("chainid", chainID) + } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, etherscanV2+"?"+params.Encode(), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL+"?"+params.Encode(), nil) if err != nil { return nil, err } diff --git a/harnesses/solana-exec/cmd/collector/main.go b/harnesses/solana-exec/cmd/collector/main.go index 496bb3e8..56cf537a 100644 --- a/harnesses/solana-exec/cmd/collector/main.go +++ b/harnesses/solana-exec/cmd/collector/main.go @@ -78,7 +78,7 @@ func collect(ctx context.Context, db *store.DB, h *helius.Client, plt string, fe } const sigLimit = 1000 - const maxPages = 30 // cap backfill at 30k sigs per account to avoid unbounded first-run pagination + const maxPages = 300 // cap backfill at 300k sigs per account (~20 days for busy wallets) var sigs []helius.SigEntry before := "" for page := 0; page < maxPages; page++ { @@ -191,10 +191,23 @@ func collect(ctx context.Context, db *store.DB, h *helius.Client, plt string, fe if feeToken == "" { // SOL-fee platform: detect via native SOL transfers. - for _, xfer := range tx.NativeTransfers { - if feeSet[xfer.ToUserAccount] { - platformFeeLamports += xfer.Amount + // Skip any tx where a fee wallet is also a sender — these are internal sweeps + // (e.g. Axiom consolidating 20 wallets) that would inflate the average fee. + hasFeeWalletSender := false + for _, sender := range tx.SenderAccounts { + if feeSet[sender] { + hasFeeWalletSender = true + break + } + } + if !hasFeeWalletSender { + for _, xfer := range tx.NativeTransfers { + if feeSet[xfer.ToUserAccount] { + platformFeeLamports += xfer.Amount + } } + } + for _, xfer := range tx.NativeTransfers { if platform.JitoTipAccounts[xfer.ToUserAccount] { jitoTipLamports += xfer.Amount isJito = true diff --git a/harnesses/solana-exec/internal/helius/client.go b/harnesses/solana-exec/internal/helius/client.go index d6ec7514..58b0743a 100644 --- a/harnesses/solana-exec/internal/helius/client.go +++ b/harnesses/solana-exec/internal/helius/client.go @@ -147,6 +147,9 @@ type EnhancedTx struct { // This is the authoritative price — use it directly, no division needed. // 0 = no priority fee set (base fee only). CUPriceDeclared int64 + // SenderAccounts is the set of accounts whose SOL balance decreased in this tx. + // Used by the collector to detect internal fee-wallet sweeps and skip them. + SenderAccounts []string } const b58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" @@ -360,7 +363,9 @@ func (c *Client) getTransaction(ctx context.Context, sig string) (EnhancedTx, er } // NativeTransfers: accounts whose SOL balance increased received SOL. + // SenderAccounts: accounts whose SOL balance decreased (used for sweep detection). var nativeTransfers []NativeTransfer + var senderAccounts []string for i, key := range accounts { if i >= len(m.PreBalances) || i >= len(m.PostBalances) { break @@ -368,6 +373,8 @@ func (c *Client) getTransaction(ctx context.Context, sig string) (EnhancedTx, er diff := m.PostBalances[i] - m.PreBalances[i] if diff > 0 { nativeTransfers = append(nativeTransfers, NativeTransfer{ToUserAccount: key.Pubkey, Amount: diff}) + } else if diff < 0 { + senderAccounts = append(senderAccounts, key.Pubkey) } } @@ -463,5 +470,6 @@ func (c *Client) getTransaction(ctx context.Context, sig string) (EnhancedTx, er ComputeUnitsConsumed: cuConsumed, CULimit: cuLimit, CUPriceDeclared: cuPriceDeclared, + SenderAccounts: senderAccounts, }, nil } diff --git a/harnesses/solana-exec/internal/platform/platforms.go b/harnesses/solana-exec/internal/platform/platforms.go index 4fa44159..a1ec8b7a 100644 --- a/harnesses/solana-exec/internal/platform/platforms.go +++ b/harnesses/solana-exec/internal/platform/platforms.go @@ -5,8 +5,18 @@ package platform // paginates each independently and aggregates before sampling. // Sources: DefiLlama dimension-adapters, verified 2026-08. var FeeAccounts = map[string][]string{ + // 8 bonding-curve fee recipients (round-robin) + Mayhem wallet. + // Sources: DeFiLlama fees/pumpdotfun adapter (master, 2026-08). "pump.fun": { "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM", + "62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV", + "FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz", + "7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX", + "AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY", + "9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz", + "G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP", + "7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ", + "GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS", // Mayhem mode }, "photon": { "AVUCZyuT35YSuj4RH7fwiyPu82Djn2Hfg7y2ND2XcnZH", diff --git a/src/app/alternatives/[slug]/page.tsx b/src/app/alternatives/[slug]/page.tsx index bd3fb2b2..05dfd25f 100644 --- a/src/app/alternatives/[slug]/page.tsx +++ b/src/app/alternatives/[slug]/page.tsx @@ -22,6 +22,7 @@ import { isRegion } from "@/lib/brand"; import { PERP_VENUE_META, benchRowsForVenue } from "@/lib/perp-venue-context"; import { fetchPerpCohort } from "@/lib/perp-stats"; import { PerpVenueBenchCards } from "@/components/perp-venue-bench-cards"; +import { LedgerTable } from "@/components/ledger-table"; export const dynamic = "force-dynamic"; @@ -341,12 +342,17 @@ export default async function AlternativePage({ )} - {/* Latency / rate / USD benches: field stats + trend chart. - The full provider table lives on the canonical benchmark page — - linking there keeps this page focused on the alternatives angle - without duplicating the complete leaderboard. */} + {/* Latency / rate / USD benches: full ranked table + trend chart. + LedgerTable gives readers the complete ranked list on this page + (better search-intent match than a CTA-only pattern) while + /benchmarks/ remains canonical for the interactive bar chart, + distribution view, and per-chain drilldowns. */} {!isDraft && bench.unit !== "count" && ( <> +
+ +
+
)} - {/* CTA to the canonical benchmark page where the full ranked - leaderboard (all providers, p50/p90/p99, success rate, trend) - lives. Keeps the alternatives page focused while giving readers - a clear path to the complete data. */} + {/* Link to canonical benchmark for interactive views not present here: + ranked bar chart, distribution, per-chain drilldowns, chart export. */} {!isDraft && (

- Full leaderboard: {bench.results.length} providers ranked by{" "} - {bench.metric.toLowerCase()}, with p50/p90/p99, success rate and - 24h trend. + Interactive bar chart, distribution view, per-chain drilldowns and + chart export on the benchmark page.

- View full benchmark + Open benchmark
diff --git a/src/app/apps/page.tsx b/src/app/apps/page.tsx index c1e78a43..c66da3b4 100644 --- a/src/app/apps/page.tsx +++ b/src/app/apps/page.tsx @@ -5,6 +5,7 @@ import { SITE } from "@/data/site"; import { fetchEVMRevenue } from "@/lib/evm-exec"; import { fetchExecLeaderboard } from "@/lib/solana-exec"; import { fetchSolPrice } from "@/lib/sol-price"; +import { fetchFOMORelayFees } from "@/lib/dune"; import { TRADING_APPS } from "@/lib/trading-apps-config"; import { TradingAppsLeaderboard, type UnifiedAppRow } from "@/components/trading-apps-leaderboard"; import { RevenueSummary } from "@/components/revenue-summary"; @@ -25,10 +26,11 @@ export const revalidate = 300; const WINDOWS = ["24h", "7d", "30d"] as const; export default async function AppsHubPage() { - const [evmData, solanaData, solPrice] = await Promise.all([ + const [evmData, solanaData, solPrice, fomoRelay] = await Promise.all([ fetchEVMRevenue(), fetchExecLeaderboard(), fetchSolPrice(), + fetchFOMORelayFees(), ]); const evmByPlatform = new Map( @@ -47,7 +49,6 @@ export default async function AppsHubPage() { const bscChain = evmRow?.chains["bsc"]; const baseChain = evmRow?.chains["base"]; - // EVM fees are always 24h — we don't have multi-window EVM data. const ethFees = ethChain ? ethChain.stable24h + (ethChain.native?.usd ?? 0) : null; const bscFees = bscChain ? bscChain.stable24h + (bscChain.native?.usd ?? 0) : null; const baseFees = baseChain ? baseChain.stable24h + (baseChain.native?.usd ?? 0) : null; @@ -63,6 +64,14 @@ export default async function AppsHubPage() { solanaFees = (wData.txCount * wData.avgPlatformFeeLamports) / 1e9 * solPrice; } } + + // FOMO: supplement on-chain fees with off-chain relay fees from Dune + // (relay accounts for ~96% of FOMO's actual revenue) + if (meta.id === "fomo" && fomoRelay) { + const relayFee = w === "24h" ? fomoRelay.fees24h : w === "7d" ? fomoRelay.fees7d : fomoRelay.fees30d; + solanaFees = (solanaFees ?? 0) + relayFee; + } + windows[w] = { solana: solanaFees, ethereum: ethFees, diff --git a/src/components/exec-chain-tabs.tsx b/src/components/exec-chain-tabs.tsx index 5b571531..61a77371 100644 --- a/src/components/exec-chain-tabs.tsx +++ b/src/components/exec-chain-tabs.tsx @@ -109,10 +109,12 @@ export function ExecChainTabs({ }) { const [chain, setChain] = useState("solana"); + const visibleTabs = evmData ? TABS : TABS.filter((t) => t.key === "solana"); + return (
- {TABS.map((tab) => ( + {visibleTabs.map((tab) => (