diff --git a/public/basescan-icon.svg b/public/basescan-icon.svg
new file mode 100644
index 00000000..a9a7d48b
--- /dev/null
+++ b/public/basescan-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/hunt-token.svg b/public/hunt-token.svg
new file mode 100644
index 00000000..e379f616
--- /dev/null
+++ b/public/hunt-token.svg
@@ -0,0 +1,11 @@
+
diff --git a/public/mc-icon-light.svg b/public/mc-icon-light.svg
new file mode 100644
index 00000000..e5a42c45
--- /dev/null
+++ b/public/mc-icon-light.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/src/app/token/page.tsx b/src/app/token/page.tsx
index be076ede..a1463f36 100644
--- a/src/app/token/page.tsx
+++ b/src/app/token/page.tsx
@@ -3,9 +3,12 @@
import { useAccount, useReadContract } from "wagmi";
import { formatUnits, erc20Abi } from "viem";
import { useState } from "react";
+import Image from "next/image";
import {
PLOT_TOKEN, EXPLORER_URL,
} from "../../../lib/contracts/constants";
+import { SwapInterface } from "../../components/token/SwapInterface";
+import { useTokenInfo, formatPrice, formatNumber } from "../../hooks/useTokenInfo";
const BASESCAN_URL = `${EXPLORER_URL}/token/${PLOT_TOKEN}`;
const MINT_CLUB_URL = "https://mint.club/token/base/PLOT";
@@ -23,14 +26,9 @@ export default function TokenPage() {
query: { enabled: !!address },
});
- const { data: totalSupply, isLoading: supplyLoading } = useReadContract({
- address: PLOT_TOKEN,
- abi: erc20Abi,
- functionName: "totalSupply",
- });
+ const { data: tokenInfo, isLoading: tokenInfoLoading } = useTokenInfo();
const formattedBalance = balance ? formatUnits(balance, 18) : "0";
- const formattedSupply = totalSupply ? formatUnits(totalSupply, 18) : "0";
const handleCopyAddress = async () => {
try {
@@ -106,6 +104,9 @@ export default function TokenPage() {
+ {/* Swap Interface */}
+
+
{/* How to Get PLOT */}
How to Get PLOT
@@ -133,65 +134,111 @@ export default function TokenPage() {
{/* Token Information */}
-
Token Information
+
Token Information
- {/* Stats */}
+ {/* Stats Grid — Price + Market Cap */}
-
-
Total Supply
- {supplyLoading ? (
+
+
Price
+ {tokenInfoLoading ? (
+ ) : tokenInfo ? (
+
+
+ {formatPrice(tokenInfo.price)}
+
+ {tokenInfo.priceChange24h !== null && (
+
= 0 ? "text-green-600" : "text-red-600"}`}>
+ {tokenInfo.priceChange24h >= 0 ? "+" : ""}
+ {tokenInfo.priceChange24h.toFixed(2)}%
+
+ )}
+
) : (
+
—
+ )}
+
+
+
Market Cap
+ {tokenInfoLoading ? (
+
+ ) : tokenInfo ? (
- {parseFloat(formattedSupply).toLocaleString(undefined, {
- minimumFractionDigits: 0,
- maximumFractionDigits: 0,
- })} PLOT
+ ${formatNumber(tokenInfo.marketCap)}
+ ) : (
+
—
)}
-
-
Network
-
-
- Base Mainnet
-
-
- {/* Links */}
+ {/* External Links */}
diff --git a/src/components/token/SwapInterface.tsx b/src/components/token/SwapInterface.tsx
new file mode 100644
index 00000000..808928ce
--- /dev/null
+++ b/src/components/token/SwapInterface.tsx
@@ -0,0 +1,126 @@
+"use client";
+
+import { useState } from "react";
+import { usePlatformDetection } from "../../hooks/usePlatformDetection";
+import { PLOT_TOKEN } from "../../../lib/contracts/constants";
+
+const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
+const UNISWAP_URL = `https://app.uniswap.org/swap?outputCurrency=${PLOT_TOKEN}&chain=base`;
+
+export function SwapInterface() {
+ const { platform, isLoading } = usePlatformDetection();
+ const [swapLoading, setSwapLoading] = useState(false);
+ const [error, setError] = useState
(null);
+
+ const handleNativeSwap = async () => {
+ try {
+ setSwapLoading(true);
+ setError(null);
+
+ const { sdk } = await import("@farcaster/miniapp-sdk");
+ const result = await sdk.actions.swapToken({
+ sellToken: `eip155:8453/erc20:${USDC_ADDRESS}`,
+ buyToken: `eip155:8453/erc20:${PLOT_TOKEN}`,
+ });
+
+ if (!result.success) {
+ if (result.reason === "rejected_by_user") {
+ setError("Swap cancelled by user");
+ } else {
+ setError("Swap failed. Please try again.");
+ }
+ }
+ } catch {
+ setError("Failed to open swap interface");
+ } finally {
+ setSwapLoading(false);
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ // Farcaster / Base App — native swap
+ if (platform === "farcaster" || platform === "base") {
+ const platformName = platform === "base" ? "Base App" : "Farcaster";
+
+ return (
+
+
+
Swap to $PLOT
+
+ {platformName}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ Opens {platformName}'s native swap interface
+
+
+ );
+ }
+
+ // Web browser — Uniswap link
+ return (
+
+ );
+}
diff --git a/src/hooks/usePlatformDetection.ts b/src/hooks/usePlatformDetection.ts
new file mode 100644
index 00000000..3f470ebf
--- /dev/null
+++ b/src/hooks/usePlatformDetection.ts
@@ -0,0 +1,42 @@
+"use client";
+
+import { useState, useEffect } from "react";
+
+export type Platform = "farcaster" | "base" | "web";
+
+/** Base App's client FID in the Farcaster protocol */
+const BASE_APP_CLIENT_FID = 309857;
+
+export function usePlatformDetection() {
+ const [platform, setPlatform] = useState("web");
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ import("@farcaster/miniapp-sdk")
+ .then(async ({ sdk }) => {
+ if (cancelled) return;
+ const context = await sdk.context;
+ if (!context?.client || cancelled) return;
+
+ if (context.client.clientFid === BASE_APP_CLIENT_FID) {
+ setPlatform("base");
+ } else {
+ setPlatform("farcaster");
+ }
+ })
+ .catch(() => {
+ // SDK not available = web browser
+ })
+ .finally(() => {
+ if (!cancelled) setIsLoading(false);
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ return { platform, isLoading, isMiniApp: platform !== "web" };
+}
diff --git a/src/hooks/useTokenInfo.ts b/src/hooks/useTokenInfo.ts
new file mode 100644
index 00000000..901e541e
--- /dev/null
+++ b/src/hooks/useTokenInfo.ts
@@ -0,0 +1,158 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { formatEther } from "viem";
+import { browserClient } from "../../lib/rpc";
+import { PLOT_TOKEN, MCV2_BOND, HUNT } from "../../lib/contracts/constants";
+
+const USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" as const;
+const ONEINCH_SPOT_PRICE_AGGREGATOR = "0x00000000000D6FFc74A8feb35aF5827bf57f6786" as const;
+
+const SPOT_PRICE_ABI = [
+ {
+ inputs: [
+ { name: "srcToken", type: "address" },
+ { name: "dstToken", type: "address" },
+ { name: "useWrappers", type: "bool" },
+ ],
+ name: "getRate",
+ outputs: [{ name: "weightedRate", type: "uint256" }],
+ stateMutability: "view",
+ type: "function",
+ },
+] as const;
+
+const MCV2_BOND_ABI = [
+ {
+ inputs: [{ name: "token", type: "address" }],
+ name: "priceForNextMint",
+ outputs: [{ name: "", type: "uint256" }],
+ stateMutability: "view",
+ type: "function",
+ },
+] as const;
+
+const ERC20_SUPPLY_ABI = [
+ {
+ inputs: [],
+ name: "totalSupply",
+ outputs: [{ name: "", type: "uint256" }],
+ stateMutability: "view",
+ type: "function",
+ },
+] as const;
+
+export interface TokenInfo {
+ price: number;
+ marketCap: number;
+ totalSupply: number;
+ priceChange24h: number | null;
+}
+
+async function getHuntPriceUSD(): Promise {
+ const weightedRate = await browserClient.readContract({
+ address: ONEINCH_SPOT_PRICE_AGGREGATOR,
+ abi: SPOT_PRICE_ABI,
+ functionName: "getRate",
+ args: [HUNT, USDC_ADDRESS, false],
+ });
+ // USDC has 6 decimals, HUNT has 18 → rate is scaled to 1e18
+ // weightedRate = how many USDC (6 dec) per 1 HUNT (18 dec) scaled by 1e18
+ // USD price = weightedRate / 1e6
+ return Number(weightedRate) / 1_000_000;
+}
+
+async function getPlotPriceUSD(): Promise {
+ const priceInHuntWei = await browserClient.readContract({
+ address: MCV2_BOND,
+ abi: MCV2_BOND_ABI,
+ functionName: "priceForNextMint",
+ args: [PLOT_TOKEN],
+ });
+ const priceInHunt = Number(formatEther(priceInHuntWei));
+ const huntPriceUSD = await getHuntPriceUSD();
+ return priceInHunt * huntPriceUSD;
+}
+
+export function useTokenInfo() {
+ return useQuery({
+ queryKey: ["plot-token-info"],
+ queryFn: async () => {
+ const [price, supply] = await Promise.all([
+ getPlotPriceUSD(),
+ browserClient.readContract({
+ address: PLOT_TOKEN,
+ abi: ERC20_SUPPLY_ABI,
+ functionName: "totalSupply",
+ }),
+ ]);
+
+ const totalSupply = Number(formatEther(supply));
+ const marketCap = price * totalSupply;
+
+ // 24h price change via block diff (~43200 blocks = 1 day on Base @ 2s)
+ let priceChange24h: number | null = null;
+ try {
+ const currentBlock = await browserClient.getBlockNumber();
+ const pastBlock = currentBlock - BigInt(43200);
+
+ const [pastPriceInHuntWei, huntPriceUSD] = await Promise.all([
+ browserClient.readContract({
+ address: MCV2_BOND,
+ abi: MCV2_BOND_ABI,
+ functionName: "priceForNextMint",
+ args: [PLOT_TOKEN],
+ blockNumber: pastBlock,
+ }),
+ getHuntPriceUSD(),
+ ]);
+
+ const pastPriceUSD = Number(formatEther(pastPriceInHuntWei)) * huntPriceUSD;
+ if (pastPriceUSD > 0) {
+ priceChange24h = ((price - pastPriceUSD) / pastPriceUSD) * 100;
+ }
+ } catch {
+ // Token may not have existed 24h ago
+ }
+
+ return { price, marketCap, totalSupply, priceChange24h } satisfies TokenInfo;
+ },
+ staleTime: 5 * 60 * 1000,
+ refetchInterval: 5 * 60 * 1000,
+ retry: 2,
+ });
+}
+
+export function formatPrice(price: number): string {
+ if (price === 0) return "$0";
+ if (price >= 1) return `$${price.toFixed(2)}`;
+
+ const str = price.toString();
+ const match = str.match(/^0\.0+/);
+
+ if (match) {
+ const leadingZeros = match[0].length - 2;
+ if (leadingZeros >= 4) {
+ const significantPart = str.slice(match[0].length);
+ const displayDigits = significantPart.slice(0, 4);
+ const subscriptMap: Record = {
+ "0": "\u2080", "1": "\u2081", "2": "\u2082", "3": "\u2083",
+ "4": "\u2084", "5": "\u2085", "6": "\u2086", "7": "\u2087",
+ "8": "\u2088", "9": "\u2089",
+ };
+ const subscriptZeros = leadingZeros.toString().split("").map((d) => subscriptMap[d]).join("");
+ return `$0.0${subscriptZeros}${displayDigits}`;
+ }
+ }
+
+ return `$${price.toFixed(6).replace(/\.?0+$/, "")}`;
+}
+
+export function formatNumber(num: number): string {
+ if (num === 0) return "0";
+ const abs = Math.abs(num);
+ if (abs >= 1e9) return `${(num / 1e9).toFixed(2)}B`;
+ if (abs >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
+ if (abs >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
+ return num.toFixed(2);
+}