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
15 changes: 11 additions & 4 deletions harnesses/evm-exec/cmd/collector/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
8 changes: 4 additions & 4 deletions harnesses/evm-exec/internal/platform/platforms.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand Down
31 changes: 21 additions & 10 deletions harnesses/evm-exec/internal/source/etherscan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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},
Expand All @@ -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
}
Expand Down
21 changes: 17 additions & 4 deletions harnesses/solana-exec/cmd/collector/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++ {
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions harnesses/solana-exec/internal/helius/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -360,14 +363,18 @@ 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
}
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)
}
}

Expand Down Expand Up @@ -463,5 +470,6 @@ func (c *Client) getTransaction(ctx context.Context, sig string) (EnhancedTx, er
ComputeUnitsConsumed: cuConsumed,
CULimit: cuLimit,
CUPriceDeclared: cuPriceDeclared,
SenderAccounts: senderAccounts,
}, nil
}
10 changes: 10 additions & 0 deletions harnesses/solana-exec/internal/platform/platforms.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 15 additions & 12 deletions src/app/alternatives/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -341,12 +342,17 @@ export default async function AlternativePage({
</div>
)}

{/* 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" && (
<>
<div className="mt-10">
<LedgerTable benchmark={bench} />
</div>

<dl className="mt-10 grid grid-cols-2 sm:flex sm:flex-wrap items-baseline gap-x-8 gap-y-3 border-y border-rule py-4">
<SummaryStat
label="Best"
Expand Down Expand Up @@ -383,22 +389,19 @@ export default async function AlternativePage({
</>
)}

{/* 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 && (
<div className="mt-10 rounded-xl card-soft px-5 py-4 flex flex-col sm:flex-row sm:items-center gap-3">
<p className="flex-1 text-sm text-ink-soft">
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.
</p>
<Link
href={benchUrl}
className="shrink-0 inline-flex items-center gap-1.5 rounded-lg bg-ink text-paper text-sm font-medium px-4 py-2 hover:opacity-80 transition-opacity"
>
View full benchmark
Open benchmark
<ArrowUpRight size={14} strokeWidth={2} />
</Link>
</div>
Expand Down
13 changes: 11 additions & 2 deletions src/app/apps/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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(
Expand All @@ -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;
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion src/components/exec-chain-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,12 @@ export function ExecChainTabs({
}) {
const [chain, setChain] = useState<Chain>("solana");

const visibleTabs = evmData ? TABS : TABS.filter((t) => t.key === "solana");

return (
<div>
<div className="flex gap-1 border-b border-rule mb-6">
{TABS.map((tab) => (
{visibleTabs.map((tab) => (
<button
key={tab.key}
type="button"
Expand Down
Loading
Loading