diff --git a/src/app/trading-apps/page.tsx b/src/app/trading-apps/page.tsx new file mode 100644 index 00000000..ea76cd87 --- /dev/null +++ b/src/app/trading-apps/page.tsx @@ -0,0 +1,483 @@ +import Link from "next/link"; +import { ProviderLogo } from "@/components/provider-logo"; +import { getBenchmark } from "@/data/benchmarks"; +import type { ProviderResult } from "@/types/benchmark"; +import { pageMetadata } from "@/lib/page-metadata"; +import { safeJsonLd, buildBreadcrumbJsonLd } from "@/lib/jsonld"; +import { SITE } from "@/data/site"; + +const DESCRIPTION = + "Live benchmarks for Solana trading platforms and Telegram bots — volume, swap transactions, average trade size, and app store ratings."; + +export const metadata: import("next").Metadata = pageMetadata({ + path: "/trading-apps", + title: "Best Solana Trading Apps 2026, ranked by benchmark", + description: DESCRIPTION, +}); + +export const revalidate = 60; + +const BENCH_SLUGS = [ + "solana-trading-platform-wars", + "solana-dex-volume", + "solana-unique-traders", + "solana-avg-trade-size", + "solana-launchpad-wars", + "memecoin-platforms", + "app-store-ratings", +] as const; + +// pump.fun is tracked via byLaunchpad (bonding curve, no referral tag system). +// All other platforms are tracked via byPlatform (referral tags). Volume is +// not directly comparable across the two groups — tooltip discloses this. +const PLATFORMS = [ + { slug: "pump-fun", name: "pump.fun" }, + { slug: "gmgn", name: "GMGN" }, + { slug: "axiom", name: "Axiom" }, + { slug: "fomo", name: "FOMO" }, + { slug: "trojan", name: "Trojan" }, + { slug: "photon", name: "Photon" }, + { slug: "maestro", name: "Maestro" }, +] as const; + +const COLUMNS = [ + { + key: "volume" as const, + label: "24h Volume", + bench: "solana-trading-platform-wars", + fmt: fmtUSD, + tip: "Mobula attribution. pump.fun = bonding-curve launchpad volume. All others = terminal routing volume via referral tag. Not directly comparable across the two groups.", + higherBetter: true, + }, + { + key: "traders" as const, + label: "Swap Tx", + bench: "solana-unique-traders", + fmt: fmtCount, + tip: "Unique swap transactions in 24h via Dune. pump.fun uses dex_solana.trades (all swaps incl. 0-fee). Terminals use fee-wallet detection (fee-generating swaps only). Methods differ.", + higherBetter: true, + }, + { + key: "tradeSize" as const, + label: "Avg Trade", + bench: "solana-avg-trade-size", + fmt: fmtUSD, + tip: "24h volume ÷ trade count via Mobula. Includes bots and MEV — platforms with heavy bot sniping (notably pump.fun) show lower averages than human-only baselines.", + higherBetter: true, + }, + { + key: "feeRate" as const, + label: "Fee Rate", + bench: "memecoin-platforms", + fmt: fmtPct, + tip: "Observed take rate: fee revenue ÷ fee-paying volume (Dune tx join). Comparable across platforms. FOMO uses DeFiLlama (includes off-chain relay fees). pump.fun cut trading fees to 0% in Aug 2026.", + higherBetter: false, + }, + { + key: "rating" as const, + label: "App Rating", + bench: "app-store-ratings", + fmt: fmtRating, + tip: "Apple App Store all-time average rating. Axiom, Trojan, Photon and Maestro have no iOS app — they show —.", + higherBetter: true, + }, +] as const; + +type ColKey = (typeof COLUMNS)[number]["key"]; + +function indexBySlug(results: ProviderResult[] | undefined): Record { + const out: Record = {}; + for (const r of results ?? []) { + out[r.slug] = r.ms.p50; + } + return out; +} + +function fmtUSD(v: number | null): string { + if (v === null) return "—"; + if (v >= 1_000_000_000) return `$${(v / 1_000_000_000).toFixed(1)}B`; + if (v >= 1_000_000) return `$${(v / 1_000_000).toFixed(0)}M`; + if (v >= 1_000) return `$${(v / 1_000).toFixed(0)}K`; + return `$${v.toFixed(0)}`; +} + +function fmtCount(v: number | null): string { + if (v === null) return "—"; + if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`; + if (v >= 1_000) return `${(v / 1_000).toFixed(0)}K`; + return v.toFixed(0); +} + +function fmtPct(v: number | null): string { + if (v === null) return "—"; + return `${v.toFixed(2)}%`; +} + +function fmtRating(v: number | null): string { + if (v === null) return "—"; + return `${v.toFixed(1)} / 5`; +} + +const GROUPS = [ + { + label: "Volume & activity", + items: [ + { slug: "solana-trading-platform-wars", title: "Trading platform volume" }, + { slug: "solana-dex-volume", title: "DEX volume & protocol revenue" }, + { slug: "solana-unique-traders", title: "Swap transactions" }, + { slug: "solana-avg-trade-size", title: "Average trade size" }, + ], + }, + { + label: "Launchpads", + items: [{ slug: "solana-launchpad-wars", title: "Launchpad volume" }], + }, + { + label: "Fees", + items: [{ slug: "memecoin-platforms", title: "Platform fee rates" }], + }, + { + label: "App store", + items: [{ slug: "app-store-ratings", title: "iOS app store ratings" }], + }, +] as const; + +export default async function TradingAppsHubPage() { + const [volBench, tradersBench, tradeSizeBench, feeBench, ratingsBench] = + await Promise.all([ + getBenchmark("solana-trading-platform-wars"), + getBenchmark("solana-unique-traders"), + getBenchmark("solana-avg-trade-size"), + getBenchmark("memecoin-platforms"), + getBenchmark("app-store-ratings"), + ]); + + const volIdx = indexBySlug(volBench?.results); + const tradersIdx = indexBySlug(tradersBench?.results); + const tradeSizeIdx = indexBySlug(tradeSizeBench?.results); + const feeIdx = indexBySlug(feeBench?.results); + const ratingIdx = indexBySlug(ratingsBench?.results); + + type Row = { + slug: string; + name: string; + volume: number | null; + traders: number | null; + tradeSize: number | null; + feeRate: number | null; + rating: number | null; + }; + + const matrix: Row[] = PLATFORMS.map((p) => ({ + slug: p.slug, + name: p.name, + volume: volIdx[p.slug] ?? null, + traders: tradersIdx[p.slug] ?? null, + tradeSize: tradeSizeIdx[p.slug] ?? null, + feeRate: feeIdx[p.slug] ?? null, + rating: ratingIdx[p.slug] ?? null, + })).sort((a, b) => (b.volume ?? -1) - (a.volume ?? -1)); + + function best(key: ColKey, higherBetter: boolean): number | null { + const vals = matrix.map((r) => r[key]).filter((v): v is number => v !== null); + if (!vals.length) return null; + return higherBetter ? Math.max(...vals) : Math.min(...vals); + } + + const bests: Partial> = {}; + for (const col of COLUMNS) { + bests[col.key] = best(col.key, col.higherBetter); + } + + const topVolumeRow = matrix.reduce( + (b, row) => ((row.volume ?? -1) > (b.volume ?? -1) ? row : b), + matrix[0], + ); + const topRating = ratingsBench?.results.find((r: ProviderResult) => + PLATFORMS.some((p) => p.slug === r.slug), + ); + + const breadcrumbLd = { + "@context": "https://schema.org", + ...buildBreadcrumbJsonLd([ + { name: "Home", item: SITE.url }, + { name: "Trading Apps", item: `${SITE.url}/trading-apps` }, + ]), + }; + + const itemListLd = { + "@context": "https://schema.org", + "@type": "ItemList", + name: "Trading app benchmarks by OpenChainBench", + description: DESCRIPTION, + numberOfItems: BENCH_SLUGS.length, + itemListElement: BENCH_SLUGS.map((slug, i) => ({ + "@type": "ListItem", + position: i + 1, + name: slug, + url: `${SITE.url}/benchmarks/${slug}`, + })), + }; + + return ( +
+