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
33 changes: 22 additions & 11 deletions src/app/apps/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const metadata: Metadata = pageMetadata({

export const revalidate = 300;

const WINDOWS = ["24h", "7d", "30d"] as const;

export default async function AppsHubPage() {
const [evmData, solanaData, solPrice] = await Promise.all([
fetchEVMRevenue(),
Expand All @@ -43,30 +45,39 @@ 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;
const evmTotal = (ethFees ?? 0) + (bscFees ?? 0) + (baseFees ?? 0);

const windows: UnifiedAppRow["windows"] = {};

let solanaFees: number | null = null;
if (solRow && solPrice !== null) {
const window24h = solRow.windows["24h"];
if (window24h) {
solanaFees = (window24h.txCount * window24h.avgPlatformFeeLamports) / 1e9 * solPrice;
for (const w of WINDOWS) {
let solanaFees: number | null = null;
if (solRow && solPrice !== null) {
const wData = solRow.windows[w];
if (wData) {
solanaFees = (wData.txCount * wData.avgPlatformFeeLamports) / 1e9 * solPrice;
}
}
windows[w] = {
solana: solanaFees,
ethereum: ethFees,
bsc: bscFees,
base: baseFees,
total: (solanaFees ?? 0) + evmTotal,
};
}

const total24h =
(solanaFees ?? 0) + (ethFees ?? 0) + (bscFees ?? 0) + (baseFees ?? 0);

return {
meta,
fees: { solana: solanaFees, ethereum: ethFees, bsc: bscFees, base: baseFees },
windows,
stableOnly: {
ethereum: ethChain?.coverage === "stable-only",
bsc: bscChain?.coverage === "stable-only",
base: baseChain?.coverage === "stable-only",
},
total24h,
};
});

Expand Down Expand Up @@ -101,7 +112,7 @@ export default async function AppsHubPage() {

<p className="mt-6 text-xs text-ink-muted leading-relaxed max-w-2xl">
Solana fees = <span className="font-mono">txCount × avgPlatformFeeLamports / 1e9 × SOL price</span>.
EVM fees = stable (USDC/USDT) + native where traceable.{" "}
EVM fees = stable (USDC/USDT) + native where traceable, always 24h.{" "}
<Link href="/apps/exec" className="underline hover:text-ink-soft transition-colors">
Detailed execution metrics →
</Link>
Expand Down
124 changes: 91 additions & 33 deletions src/components/trading-apps-leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
"use client";

import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { logoPath } from "@/lib/logo-manifest";
import type { AppMeta } from "@/lib/trading-apps-config";

type FeeWindow = {
solana: number | null;
ethereum: number | null;
bsc: number | null;
base: number | null;
total: number;
};

export type UnifiedAppRow = {
meta: AppMeta;
fees: {
solana: number | null;
ethereum: number | null;
bsc: number | null;
base: number | null;
};
windows: Record<string, FeeWindow>;
stableOnly: { ethereum: boolean; bsc: boolean; base: boolean };
total24h: number;
};

type TabKey = "all" | "meme-bot" | "telegram-bot";
type WindowKey = "24h" | "7d" | "30d";

const TABS: { key: TabKey; label: string }[] = [
{ key: "all", label: "All" },
{ key: "meme-bot", label: "Meme Bots" },
{ key: "telegram-bot", label: "Telegram Bots" },
];

const WINDOWS: { key: WindowKey; label: string }[] = [
{ key: "24h", label: "24h" },
{ key: "7d", label: "7d" },
{ key: "30d", label: "30d" },
];

const CATEGORY_BADGE: Record<AppMeta["category"], string> = {
"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",
Expand Down Expand Up @@ -79,9 +89,12 @@ export function TradingAppsLeaderboard({
updatedAt: string | null;
}) {
const [tab, setTab] = useState<TabKey>("all");
const [window, setWindow] = useState<WindowKey>("24h");

const filtered = tab === "all" ? rows : rows.filter((r) => r.meta.category === tab);
const sorted = [...filtered].sort((a, b) => b.total24h - a.total24h);
const sorted = [...filtered].sort(
(a, b) => (b.windows[window]?.total ?? 0) - (a.windows[window]?.total ?? 0)
);

const hasStableOnly = sorted.some(
(r) => r.stableOnly.ethereum || r.stableOnly.bsc || r.stableOnly.base
Expand All @@ -106,11 +119,29 @@ export function TradingAppsLeaderboard({
</button>
))}
</div>
{updatedAt && (
<span className="text-[11px] text-ink-faint font-mono pb-2">
Updated {new Date(updatedAt).toLocaleString()}
</span>
)}
<div className="flex items-center gap-3 pb-2">
<div className="flex gap-0.5 bg-paper-soft rounded-md p-0.5">
{WINDOWS.map((w) => (
<button
key={w.key}
type="button"
onClick={() => setWindow(w.key)}
className={`px-2.5 py-1 text-xs font-mono rounded transition-colors ${
window === w.key
? "bg-paper text-ink shadow-sm"
: "text-ink-muted hover:text-ink-soft"
}`}
>
{w.label}
</button>
))}
</div>
{updatedAt && (
<span className="text-[11px] text-ink-faint font-mono">
{new Date(updatedAt).toLocaleString()}
</span>
)}
</div>
</div>

<div className="overflow-x-auto">
Expand All @@ -120,16 +151,23 @@ export function TradingAppsLeaderboard({
<th className="text-left py-3 pl-4 sm:pl-6 pr-3 font-medium text-ink-muted text-xs w-8">#</th>
<th className="text-left py-3 pr-4 font-medium text-ink-muted text-xs">App</th>
<th className="text-left py-3 pr-4 font-medium text-ink-muted text-xs hidden sm:table-cell">Category</th>
<th className="text-right py-3 pr-4 sm:pr-6 font-medium text-ink-muted text-xs">Solana</th>
<th className="text-right py-3 pr-4 sm:pr-6 font-medium text-ink-muted text-xs">
<Link href="/benchmarks/memecoin-platforms" className="hover:text-ink-soft transition-colors underline decoration-dotted">
Solana
</Link>
</th>
<th className="text-right py-3 pr-4 sm:pr-6 font-medium text-ink-muted text-xs hidden md:table-cell">Ethereum</th>
<th className="text-right py-3 pr-4 sm:pr-6 font-medium text-ink-muted text-xs hidden md:table-cell">BSC</th>
<th className="text-right py-3 pr-4 sm:pr-6 font-medium text-ink-muted text-xs hidden md:table-cell">Base</th>
<th className="text-right py-3 pr-4 sm:pr-6 font-medium text-ink-muted text-xs">Total 24h</th>
<th className="text-right py-3 pr-4 sm:pr-6 font-medium text-ink-muted text-xs">
Total {window}
</th>
</tr>
</thead>
<tbody>
{sorted.map((row, i) => {
const { meta, fees, stableOnly } = row;
const { meta, stableOnly } = row;
const fees = row.windows[window] ?? { solana: null, ethereum: null, bsc: null, base: null, total: 0 };
const logo = meta.logoKey ? logoPath(meta.logoKey) : null;

return (
Expand All @@ -141,23 +179,37 @@ export function TradingAppsLeaderboard({
{i + 1}
</td>
<td className="py-4 pr-4 align-middle">
<a
href={meta.productUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2.5 hover:opacity-80 transition-opacity"
>
{logo && (
<Image
src={logo}
alt={meta.name}
width={22}
height={22}
className="rounded-full shrink-0 object-contain bg-paper-soft"
/>
<div className="flex items-center gap-2.5">
<Link
href={meta.benchUrl ?? meta.productUrl}
{...(!meta.benchUrl ? { target: "_blank", rel: "noopener noreferrer" } : {})}
className="flex items-center gap-2.5 hover:opacity-80 transition-opacity"
>
{logo && (
<Image
src={logo}
alt={meta.name}
width={22}
height={22}
className="rounded-full shrink-0 object-contain bg-paper-soft"
/>
)}
<span className="font-medium text-ink">{meta.name}</span>
</Link>
{meta.benchUrl && (
<a
href={meta.productUrl}
target="_blank"
rel="noopener noreferrer"
className="text-ink-faint hover:text-ink-muted transition-colors"
title={`Visit ${meta.name}`}
>
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M3.5 3H2a1 1 0 00-1 1v6a1 1 0 001 1h6a1 1 0 001-1V8.5M7 1h4m0 0v4m0-4L5.5 6.5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</a>
)}
<span className="font-medium text-ink">{meta.name}</span>
</a>
</div>
</td>
<td className="py-4 pr-4 align-middle hidden sm:table-cell">
<span className={`inline-flex px-2 py-0.5 rounded text-[11px] font-medium ${CATEGORY_BADGE[meta.category]}`}>
Expand All @@ -169,7 +221,7 @@ export function TradingAppsLeaderboard({
<ChainCell value={fees.bsc} stableOnly={stableOnly.bsc} />
<ChainCell value={fees.base} stableOnly={stableOnly.base} />
<td className="py-4 pr-4 sm:pr-6 text-right font-mono font-semibold text-ink tabular-nums align-middle">
{row.total24h > 0 ? fmtUSD(row.total24h) : <Dash />}
{fees.total > 0 ? fmtUSD(fees.total) : <Dash />}
</td>
</tr>
);
Expand All @@ -181,6 +233,12 @@ export function TradingAppsLeaderboard({
{hasStableOnly && (
<p className="px-4 sm:px-6 py-2.5 text-[11px] text-ink-faint border-t border-rule">
<sup>°</sup> USDC only (no native trace on Base).
{window !== "24h" && " EVM fees are always 24h — only Solana has multi-window data."}
</p>
)}
{!hasStableOnly && window !== "24h" && (
<p className="px-4 sm:px-6 py-2.5 text-[11px] text-ink-faint border-t border-rule">
EVM fees (ETH/BSC/Base) are always 24h — only Solana has multi-window data.
</p>
)}
</div>
Expand Down
18 changes: 9 additions & 9 deletions src/lib/trading-apps-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ export type AppMeta = {
};

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" },
{ 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" },
{ id: "pump.fun", name: "pump.fun", category: "meme-bot", logoKey: "pump-fun", productUrl: "https://pump.fun", benchUrl: "/apps/exec", evmKey: "pumpfun", solanaKey: "pump.fun" },
{ id: "fomo", name: "FOMO", category: "meme-bot", logoKey: "fomo", productUrl: "https://fomo.fund", benchUrl: "/apps/exec", evmKey: null, solanaKey: "fomo" },
{ id: "bullx", name: "BullX", category: "meme-bot", logoKey: "bullx", productUrl: "https://bullx.io", benchUrl: "/apps/exec", evmKey: null, solanaKey: "bullx" },
{ id: "photon", name: "Photon", category: "meme-bot", logoKey: "photon", productUrl: "https://photon-sol.tinyastro.io", benchUrl: "/apps/exec", evmKey: null, solanaKey: "photon" },
{ id: "gmgn", name: "GMGN", category: "telegram-bot", logoKey: "gmgn", productUrl: "https://gmgn.ai", benchUrl: "/apps/exec", evmKey: "gmgn", solanaKey: "gmgn" },
{ id: "axiom", name: "Axiom", category: "telegram-bot", logoKey: "axiom", productUrl: "https://axiom.trade", benchUrl: "/apps/exec", evmKey: "axiom", solanaKey: "axiom" },
{ id: "maestro", name: "Maestro", category: "telegram-bot", logoKey: "maestro", productUrl: "https://maestro.bots.gg", benchUrl: "/apps/exec", evmKey: "maestro", solanaKey: null },
{ id: "banana-gun", name: "Banana Gun", category: "telegram-bot", logoKey: "banana-gun", productUrl: "https://t.me/BananaGunSniper_bot", benchUrl: "/apps/exec", evmKey: "banana-gun", solanaKey: null },
{ id: "trojan", name: "Trojan", category: "telegram-bot", logoKey: "trojan", productUrl: "https://trojan.bot", benchUrl: "/apps/exec", evmKey: null, solanaKey: "trojan" },
];
Loading