From 66d2158e8315462b6911e1a7a138dcb89ba5bc36 Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 11:51:39 -0700 Subject: [PATCH 01/46] feat(partners): admin-approved USDC payout engine (Base) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the missing payout loop for partner referral commissions — previously "auto-issued in USDC" was copy-only; commissions sat pending forever. - lib/usdc-payout.ts: viem-based Base USDC sender. Dry-run + Base Sepolia by default; never moves real funds until PAYOUT_DRY_RUN=false and PAYOUT_NETWORK=base. Address validation (rejects zero/placeholder), hot-wallet balance check. - kol-registry.ts: add `processing` status + commission lock/release (prevents double-send), payout-attempt audit log, listPendingCommissions. - api/admin/payouts: GET pending grouped by partner w/ wallet validation + hot wallet balance; POST executes a batch with per-batch USD cap, balance guard, per-commission lock, and full audit record. - partners-proxy: expose payouts (GET) + run-payouts (POST) behind email-session admin auth; records approving admin in the audit. - admin/partners: Payouts section — mode banner (DRY RUN/LIVE), hot wallet status, per-partner Approve & Pay + Pay-all, invalid wallets blocked from payout. - partner dashboard: render the new `processing` status. - .env.example: PAYOUT_* vars documented with safety defaults. Phase 1 of the partner program build. Payout-channel hardening, partner-facing templates, and social/hashtag tracking follow. Co-Authored-By: Claude Opus 4.8 --- .env.example | 21 ++ apps/web/src/app/admin/partners/page.tsx | 140 +++++++++++ .../src/app/api/admin/partners-proxy/route.ts | 26 ++ apps/web/src/app/api/admin/payouts/route.ts | 237 ++++++++++++++++++ apps/web/src/app/partners/dashboard/page.tsx | 3 +- apps/web/src/lib/kol-registry.ts | 69 ++++- apps/web/src/lib/usdc-payout.ts | 170 +++++++++++++ 7 files changed, 664 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/api/admin/payouts/route.ts create mode 100644 apps/web/src/lib/usdc-payout.ts diff --git a/.env.example b/.env.example index decba59..07fd9d2 100644 --- a/.env.example +++ b/.env.example @@ -75,3 +75,24 @@ MAGIC_IDP_AUDIENCE=your-app-audience MAGIC_IDP_JWKS_URI=https://your-auth-provider.com/.well-known/jwks.json # Protects admin Magic routes (x-malama-ops-key or Authorization: Bearer) MALAMA_OPS_KEY=change_me_long_random_secret + +# ── Partner referral payouts (USDC on Base) ────────────────────────────────── +# Admin-approved, system-sent commissions. SAFE BY DEFAULT: dry-run ON and +# testnet selected until you explicitly opt in. See lib/usdc-payout.ts. +# +# Dedicated payout HOT WALLET key — fund with operating float ONLY, never a +# treasury/multisig. Server env only; never commit. Needs USDC + a little ETH +# for gas on the selected network. +PAYOUT_WALLET_PRIVATE_KEY=0x_dedicated_payout_wallet_key +# "base-sepolia" (default, testnet) | "base" (mainnet, real money) +PAYOUT_NETWORK=base-sepolia +# Base mainnet RPC (used only when PAYOUT_NETWORK=base) +BASE_RPC_URL=https://mainnet.base.org +# Must be exactly "false" to send real USDC. Anything else = dry run (no chain tx). +PAYOUT_DRY_RUN=true +# Hard ceiling per payout batch in USD — batches above this are rejected. +PAYOUT_MAX_BATCH_USD=5000 +# Admin API secret (x-admin-secret) — required for /api/admin/* including payouts. +ADMIN_SECRET=change_me_long_random_secret +# Comma-separated admin emails allowed into the admin dashboard. +ADMIN_EMAILS=tyler@malamaproject.org diff --git a/apps/web/src/app/admin/partners/page.tsx b/apps/web/src/app/admin/partners/page.tsx index 2238155..8ad2b4c 100644 --- a/apps/web/src/app/admin/partners/page.tsx +++ b/apps/web/src/app/admin/partners/page.tsx @@ -47,6 +47,24 @@ interface CopyTemplate { body: string; } +interface PayoutGroup { + kolId: string; + displayName: string; + walletAddress: string | null; + walletValid: boolean; + walletReason?: string; + commissionCount: number; + totalUsd: number; +} + +interface PayoutsData { + config: { network: string; dryRun: boolean; maxBatchUsd: number; hasWallet: boolean }; + hotWallet: { address: string; usdc: number; eth: string } | null; + totalPendingUsd: number; + pendingCount: number; + groups: PayoutGroup[]; +} + // ── Helpers ─────────────────────────────────────────────────────────────────── const APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? 'https://launch.malamalabs.com'; @@ -76,6 +94,15 @@ export default function AdminPartnersPage() { const [showInvite, setShowInvite] = useState(false); const [selectedTemplate, setSelectedTemplate] = useState(null); const [copiedId, setCopiedId] = useState(null); + const [payouts, setPayouts] = useState(null); + const [payoutBusy, setPayoutBusy] = useState(null); + const [payoutMsg, setPayoutMsg] = useState(null); + + const loadPayouts = () => + fetch('/api/admin/partners-proxy?action=payouts') + .then((r) => r.json()) + .then((d) => { if (!d.error) setPayouts(d as PayoutsData); }) + .catch(() => {}); // ── Load data ── useEffect(() => { @@ -90,6 +117,7 @@ export default function AdminPartnersPage() { }) .catch((e) => setError(String(e))) .finally(() => setLoading(false)); + loadPayouts(); }, []); const copy = (text: string, id: string) => { @@ -112,6 +140,41 @@ export default function AdminPartnersPage() { } }; + const runPayout = async (group?: PayoutGroup) => { + if (!payouts) return; + const dry = payouts.config.dryRun; + const what = group + ? `$${group.totalUsd} to ${group.displayName}` + : `all pending ($${payouts.totalPendingUsd})`; + if (!window.confirm(`${dry ? '[DRY RUN] ' : '⚠️ LIVE — '}Send ${what} in USDC on ${payouts.config.network}?`)) return; + setPayoutBusy(group?.kolId ?? 'all'); + setPayoutMsg(null); + try { + const res = await fetch('/api/admin/partners-proxy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'run-payouts', ...(group ? { kolId: group.kolId } : {}) }), + }); + const data = await res.json(); + if (!res.ok) { + setPayoutMsg(`✕ ${data.error ?? 'Payout failed'}`); + } else { + setPayoutMsg( + `${data.dryRun ? '[DRY RUN] ' : ''}Paid ${data.paidCount} · failed ${data.failedCount} · skipped ${data.skippedCount} · $${data.paidUsd}` + ); + await loadPayouts(); + fetch('/api/admin/partners-proxy?action=list') + .then((r) => r.json()) + .then((d) => { if (!d.error) setPartners((d.partners ?? d.kols ?? []) as PartnerWithStats[]); }) + .catch(() => {}); + } + } catch (e) { + setPayoutMsg(`✕ ${String(e)}`); + } finally { + setPayoutBusy(null); + } + }; + if (loading) return

Loading partner registry…

; if (error === 'Forbidden') return

Access denied — admin only.

; if (error) return

Error: {error}

; @@ -136,6 +199,83 @@ export default function AdminPartnersPage() { + {/* ── Payouts ── */} + {payouts && ( +
+
+ + {payouts.config.dryRun ? 'DRY RUN' : '● LIVE'} + + network: {payouts.config.network} + cap: ${payouts.config.maxBatchUsd}/batch + + wallet: {payouts.hotWallet + ? `${payouts.hotWallet.address.slice(0, 6)}…${payouts.hotWallet.address.slice(-4)} · ${payouts.hotWallet.usdc} USDC · ${Number(payouts.hotWallet.eth).toFixed(4)} ETH` + : (payouts.config.hasWallet ? 'configured' : '⚠ not configured')} + +
+ + {payoutMsg && ( +

+ {payoutMsg} +

+ )} + +

+ {payouts.pendingCount} pending commission{payouts.pendingCount === 1 ? '' : 's'} · ${payouts.totalPendingUsd} total +

+ + {payouts.groups.length === 0 && ( +

No pending commissions to pay.

+ )} + + {payouts.groups.map((g) => ( +
+
+

{g.displayName}

+

+ {g.commissionCount} sale{g.commissionCount === 1 ? '' : 's'} ·{' '} + {g.walletValid + ? `${g.walletAddress!.slice(0, 6)}…${g.walletAddress!.slice(-4)}` + : ⚠ {g.walletReason}} +

+
+
+ ${g.totalUsd} + +
+
+ ))} + + {payouts.groups.some((g) => g.walletValid) && ( + + )} +
+ )} + {/* ── Pending approvals ── */} {pending.length > 0 && (
diff --git a/apps/web/src/app/api/admin/partners-proxy/route.ts b/apps/web/src/app/api/admin/partners-proxy/route.ts index eeffbba..e705c78 100644 --- a/apps/web/src/app/api/admin/partners-proxy/route.ts +++ b/apps/web/src/app/api/admin/partners-proxy/route.ts @@ -51,6 +51,15 @@ async function kolFetch(path: string, init?: RequestInit) { }); } +/** Fetch any server-side admin API (keeps ADMIN_SECRET off the client). */ +async function adminApiFetch(path: string, init?: RequestInit) { + const secret = process.env.ADMIN_SECRET ?? ''; + return fetch(`${KOL_BASE}${path}`, { + ...init, + headers: { ...(init?.headers ?? {}), 'x-admin-secret': secret, 'Content-Type': 'application/json' }, + }); +} + // ── Approved copy templates ─────────────────────────────────────────────────── // Not exported — Next.js route files only allow HTTP-verb exports. @@ -146,6 +155,12 @@ export async function GET(req: NextRequest) { return NextResponse.json({ templates: APPROVED_COPY_TEMPLATES }); } + if (action === 'payouts') { + const res = await adminApiFetch('/api/admin/payouts'); + const data = await res.json(); + return NextResponse.json(data, { status: res.status }); + } + return NextResponse.json({ error: 'Unknown action' }, { status: 400 }); } @@ -192,5 +207,16 @@ export async function POST(req: NextRequest) { return NextResponse.json(data, { status: res.status }); } + if (action === 'run-payouts') { + // Execute a payout batch. The approving admin's email is recorded in the audit. + const { commissionIds, kolId } = body as { commissionIds?: string[]; kolId?: string }; + const res = await adminApiFetch('/api/admin/payouts', { + method: 'POST', + body: JSON.stringify({ approvedBy: email, commissionIds, kolId }), + }); + const data = await res.json(); + return NextResponse.json(data, { status: res.status }); + } + return NextResponse.json({ error: 'Unknown action' }, { status: 400 }); } diff --git a/apps/web/src/app/api/admin/payouts/route.ts b/apps/web/src/app/api/admin/payouts/route.ts new file mode 100644 index 0000000..09bd2bc --- /dev/null +++ b/apps/web/src/app/api/admin/payouts/route.ts @@ -0,0 +1,237 @@ +/** + * Admin payout API — admin-approved, system-sent USDC commissions on Base. + * + * GET /api/admin/payouts → pending commissions grouped by partner, wallet + * validation, hot-wallet balance, and payout config. + * POST /api/admin/payouts → execute a payout batch. + * body: { approvedBy: string, commissionIds?: string[], kolId?: string } + * - commissionIds: explicit subset to pay + * - kolId: pay all pending for one partner + * - neither: pay ALL pending (still capped by PAYOUT_MAX_BATCH_USD) + * + * Auth: `x-admin-secret: ` header (same as /api/admin/kol). + * Safety: dry-run + Base Sepolia by default (see lib/usdc-payout.ts). Per-commission + * lock (pending→processing) prevents double-send; batch is rejected if it exceeds + * the USD cap or the hot wallet lacks USDC. + */ + +import { NextResponse } from 'next/server' +import { randomUUID } from 'crypto' +import { + listPendingCommissions, + getKOL, + lockCommissionForPayout, + releaseCommissionLock, + markCommissionPaid, + recordPayoutAttempt, + type PayoutAttempt, + type ReferralCommission, +} from '@/lib/kol-registry' +import { + getPayoutConfig, + validatePayoutAddress, + getHotWalletStatus, + sendUsdc, +} from '@/lib/usdc-payout' + +export const runtime = 'nodejs' + +function checkAdmin(req: Request): boolean { + const secret = process.env.ADMIN_SECRET?.trim() + if (!secret) return false + return req.headers.get('x-admin-secret')?.trim() === secret +} + +// GET — pending payouts overview +export async function GET(req: Request) { + if (!checkAdmin(req)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const config = getPayoutConfig() + const [pending, hotWallet] = await Promise.all([listPendingCommissions(), getHotWalletStatus()]) + + // Group pending commissions by partner, attach wallet validation. + const byKol = new Map() + for (const c of pending) { + const arr = byKol.get(c.kolId) ?? [] + arr.push(c) + byKol.set(c.kolId, arr) + } + + const groups = await Promise.all( + Array.from(byKol.entries()).map(async ([kolId, commissions]) => { + const partner = await getKOL(kolId) + const wallet = partner?.walletAddress + const validation = validatePayoutAddress(wallet) + const totalUsd = Math.round(commissions.reduce((s, c) => s + c.commissionUsd, 0) * 100) / 100 + return { + kolId, + displayName: partner?.displayName ?? kolId, + walletAddress: wallet ?? null, + walletValid: validation.ok, + walletReason: validation.reason, + commissionCount: commissions.length, + totalUsd, + commissions, + } + }) + ) + + const totalPendingUsd = Math.round(groups.reduce((s, g) => s + g.totalUsd, 0) * 100) / 100 + + return NextResponse.json({ + config: { + network: config.network, + dryRun: config.dryRun, + maxBatchUsd: config.maxBatchUsd, + hasWallet: config.hasWallet, + usdcAddress: config.usdcAddress, + }, + hotWallet, // null if no key configured + totalPendingUsd, + pendingCount: pending.length, + groups: groups.sort((a, b) => b.totalUsd - a.totalUsd), + }) +} + +// POST — execute a batch +export async function POST(req: Request) { + if (!checkAdmin(req)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + let body: { approvedBy?: string; commissionIds?: string[]; kolId?: string } + try { + body = await req.json() + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) + } + + const approvedBy = (body.approvedBy ?? 'unknown').toString() + const config = getPayoutConfig() + + // Resolve target commissions (always re-read pending from source of truth). + let pending = await listPendingCommissions() + if (Array.isArray(body.commissionIds) && body.commissionIds.length) { + const want = new Set(body.commissionIds) + pending = pending.filter((c) => want.has(c.id)) + } else if (body.kolId) { + pending = pending.filter((c) => c.kolId === body.kolId) + } + + if (!pending.length) { + return NextResponse.json({ error: 'No matching pending commissions' }, { status: 400 }) + } + + // Validate wallets up front; separate payable from skipped. + const payable: Array<{ commission: ReferralCommission; to: string }> = [] + const skipped: PayoutAttempt['results'] = [] + for (const c of pending) { + const partner = await getKOL(c.kolId) + const v = validatePayoutAddress(partner?.walletAddress) + if (!v.ok || !v.checksum) { + skipped.push({ + commissionId: c.id, + kolId: c.kolId, + toAddress: partner?.walletAddress ?? '', + amountUsd: c.commissionUsd, + status: 'skipped', + error: v.reason ?? 'Invalid wallet', + }) + continue + } + payable.push({ commission: c, to: v.checksum }) + } + + const totalUsd = Math.round(payable.reduce((s, p) => s + p.commission.commissionUsd, 0) * 100) / 100 + + // Hard per-batch ceiling — reject rather than partial-send silently. + if (totalUsd > config.maxBatchUsd) { + return NextResponse.json( + { + error: `Batch total $${totalUsd} exceeds PAYOUT_MAX_BATCH_USD ($${config.maxBatchUsd}). Split the batch.`, + }, + { status: 400 } + ) + } + + // Live mode: ensure the hot wallet can cover the batch before locking anything. + if (!config.dryRun) { + const hot = await getHotWalletStatus() + if (!hot) { + return NextResponse.json({ error: 'PAYOUT_WALLET_PRIVATE_KEY not configured' }, { status: 400 }) + } + if (hot.usdc < totalUsd) { + return NextResponse.json( + { error: `Hot wallet has ${hot.usdc} USDC, batch needs ${totalUsd}. Top up first.` }, + { status: 400 } + ) + } + } + + // Execute. Lock each commission first (pending→processing) to prevent double-send. + const results: PayoutAttempt['results'] = [...skipped] + for (const { commission, to } of payable) { + const locked = await lockCommissionForPayout(commission.id) + if (!locked) { + results.push({ + commissionId: commission.id, + kolId: commission.kolId, + toAddress: to, + amountUsd: commission.commissionUsd, + status: 'skipped', + error: 'Already processed or locked', + }) + continue + } + try { + const { txHash } = await sendUsdc(to, commission.commissionUsd) + await markCommissionPaid(commission.id, txHash) + results.push({ + commissionId: commission.id, + kolId: commission.kolId, + toAddress: to, + amountUsd: commission.commissionUsd, + status: 'paid', + txHash, + }) + } catch (e) { + await releaseCommissionLock(commission.id) + results.push({ + commissionId: commission.id, + kolId: commission.kolId, + toAddress: to, + amountUsd: commission.commissionUsd, + status: 'failed', + error: e instanceof Error ? e.message : String(e), + }) + } + } + + const paidCount = results.filter((r) => r.status === 'paid').length + const failedCount = results.filter((r) => r.status === 'failed').length + const paidUsd = + Math.round(results.filter((r) => r.status === 'paid').reduce((s, r) => s + r.amountUsd, 0) * 100) / 100 + + const attempt: PayoutAttempt = { + id: randomUUID(), + approvedBy, + network: config.network, + dryRun: config.dryRun, + results, + totalUsd: paidUsd, + paidCount, + failedCount, + createdAt: Date.now(), + } + await recordPayoutAttempt(attempt) + + return NextResponse.json({ + ok: true, + dryRun: config.dryRun, + network: config.network, + paidCount, + failedCount, + skippedCount: results.filter((r) => r.status === 'skipped').length, + paidUsd, + attemptId: attempt.id, + results, + }) +} diff --git a/apps/web/src/app/partners/dashboard/page.tsx b/apps/web/src/app/partners/dashboard/page.tsx index faa59fa..4490193 100644 --- a/apps/web/src/app/partners/dashboard/page.tsx +++ b/apps/web/src/app/partners/dashboard/page.tsx @@ -56,8 +56,9 @@ function CopyButton({ text, label }: { text: string; label?: string }) { } function StatusBadge({ status }: { status: ReferralCommission['status'] }) { - const map = { + const map: Record = { pending: 'bg-amber-500/10 text-amber-400 border-amber-500/20', + processing: 'bg-sky-500/10 text-sky-400 border-sky-500/20', paid: 'bg-malama-accent/10 text-malama-accent border-malama-accent/20', cancelled: 'bg-red-500/10 text-red-400 border-red-500/20', } diff --git a/apps/web/src/lib/kol-registry.ts b/apps/web/src/lib/kol-registry.ts index 6c83663..b65e569 100644 --- a/apps/web/src/lib/kol-registry.ts +++ b/apps/web/src/lib/kol-registry.ts @@ -46,7 +46,8 @@ export type ReferralCommission = { /** Calculated: saleAmountUsd × commissionBps / 10_000 */ commissionUsd: number commissionBps: number - status: 'pending' | 'paid' | 'cancelled' + /** processing = locked while a payout batch is executing (prevents double-send) */ + status: 'pending' | 'processing' | 'paid' | 'cancelled' /** Set after USDC payout tx on Base */ txHash?: string stripeSessionId?: string @@ -54,6 +55,29 @@ export type ReferralCommission = { paidAt?: number } +/** Audit record for a single payout batch run (admin-approved, system-sent). */ +export type PayoutAttempt = { + id: string + /** Admin email that approved the batch */ + approvedBy: string + network: 'base' | 'base-sepolia' + dryRun: boolean + /** Per-commission outcomes */ + results: Array<{ + commissionId: string + kolId: string + toAddress: string + amountUsd: number + status: 'paid' | 'failed' | 'skipped' + txHash?: string + error?: string + }> + totalUsd: number + paidCount: number + failedCount: number + createdAt: number +} + // ── KV key helpers ─────────────────────────────────────────────────────────── const K = { @@ -64,6 +88,8 @@ const K = { commissionsByKol: (kolId: string) => `kol:commissions:by-kol:${kolId}`, commissionsAll: 'kol:commissions:all', clicks: (kolId: string) => `kol:clicks:${kolId}`, + payoutAttempt: (id: string) => `kol:payout:${id}`, + payoutAttemptsAll: 'kol:payouts:all', } // ── Partner CRUD ───────────────────────────────────────────────────────────── @@ -203,6 +229,47 @@ export async function cancelCommission(id: string): Promise { + const all = await listAllCommissions() + return all.filter((c) => c.status === 'pending').sort((a, b) => a.createdAt - b.createdAt) +} + +/** + * Atomically-ish claim a commission for payout: pending → processing. + * Returns the locked commission, or null if it was not in `pending` (already + * paid / cancelled / being processed by another run). Guards against double-send. + */ +export async function lockCommissionForPayout(id: string): Promise { + const commission = await kv.get(K.commission(id)) + if (!commission || commission.status !== 'pending') return null + const locked: ReferralCommission = { ...commission, status: 'processing' } + await kv.set(K.commission(id), locked) + return locked +} + +/** Release a lock back to `pending` after a failed send so it can be retried. */ +export async function releaseCommissionLock(id: string): Promise { + const commission = await kv.get(K.commission(id)) + if (commission && commission.status === 'processing') { + await kv.set(K.commission(id), { ...commission, status: 'pending' }) + } +} + +export async function recordPayoutAttempt(attempt: PayoutAttempt): Promise { + await kv.set(K.payoutAttempt(attempt.id), attempt) + await kv.sadd(K.payoutAttemptsAll, attempt.id) +} + +export async function listPayoutAttempts(): Promise { + const ids = await kv.smembers(K.payoutAttemptsAll) + if (!ids.length) return [] + const results = await Promise.all(ids.map((id) => kv.get(K.payoutAttempt(id)))) + return (results.filter(Boolean) as PayoutAttempt[]).sort((a, b) => b.createdAt - a.createdAt) +} + // ── Click tracking ─────────────────────────────────────────────────────────── export async function trackKOLClick(kolId: string): Promise { diff --git a/apps/web/src/lib/usdc-payout.ts b/apps/web/src/lib/usdc-payout.ts new file mode 100644 index 0000000..e85d9e6 --- /dev/null +++ b/apps/web/src/lib/usdc-payout.ts @@ -0,0 +1,170 @@ +/** + * Base USDC payout engine (server-only). + * + * Sends partner referral commissions in USDC on Base. Built for the + * "admin-approved, system-sends" model: the admin approves a batch, this module + * executes the on-chain transfers from a dedicated hot wallet. + * + * SAFETY DEFAULTS — nothing real moves until you opt in: + * - Dry-run is ON by default (PAYOUT_DRY_RUN must be explicitly "false"). + * - Network defaults to Base Sepolia testnet (PAYOUT_NETWORK="base" for mainnet). + * - Per-batch USD ceiling enforced by the caller via getPayoutConfig().maxBatchUsd. + * + * Env: + * PAYOUT_WALLET_PRIVATE_KEY 0x-key of a DEDICATED hot wallet (operating float only, + * NOT a treasury/multisig). Server env only — never commit. + * PAYOUT_NETWORK "base-sepolia" (default) | "base" + * BASE_RPC_URL Base mainnet RPC (default https://mainnet.base.org) + * BASE_SEPOLIA_RPC_URL Base Sepolia RPC (default https://sepolia.base.org) + * PAYOUT_DRY_RUN "false" to send for real; anything else = dry run + * PAYOUT_MAX_BATCH_USD hard ceiling per batch (default 5000) + */ + +import { + createPublicClient, + createWalletClient, + http, + isAddress, + getAddress, + parseUnits, + formatUnits, + erc20Abi, + type Hex, +} from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { base, baseSepolia } from 'viem/chains' + +export type PayoutNetwork = 'base' | 'base-sepolia' + +// Canonical USDC token contracts (6 decimals on both). +const USDC_ADDRESS: Record = { + base: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + 'base-sepolia': '0x036CbD53842c5426634e7929541eC2318f3dCF7e', +} +const USDC_DECIMALS = 6 +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + +export type PayoutConfig = { + network: PayoutNetwork + dryRun: boolean + maxBatchUsd: number + /** Whether a payout wallet key is configured (does not expose the key). */ + hasWallet: boolean + usdcAddress: Hex +} + +function parseBool(v: string | undefined, dflt: boolean): boolean { + if (v === undefined) return dflt + return v.trim().toLowerCase() === 'true' +} + +function normalizeKey(raw: string | undefined): Hex | null { + if (!raw) return null + const k = raw.trim().replace(/^["']|["']$/g, '') + if (!k) return null + return (k.startsWith('0x') ? k : `0x${k}`) as Hex +} + +export function getPayoutConfig(): PayoutConfig { + const network: PayoutNetwork = process.env.PAYOUT_NETWORK?.trim() === 'base' ? 'base' : 'base-sepolia' + // Dry run defaults TRUE — must be explicitly disabled. + const dryRun = parseBool(process.env.PAYOUT_DRY_RUN, true) + const maxBatchUsd = Number.parseFloat(process.env.PAYOUT_MAX_BATCH_USD ?? '5000') || 5000 + return { + network, + dryRun, + maxBatchUsd, + hasWallet: normalizeKey(process.env.PAYOUT_WALLET_PRIVATE_KEY) !== null, + usdcAddress: USDC_ADDRESS[network], + } +} + +/** Validate a partner payout address. Rejects malformed and zero addresses. */ +export function validatePayoutAddress( + address: string | undefined | null +): { ok: boolean; checksum?: Hex; reason?: string } { + if (!address) return { ok: false, reason: 'No wallet address on file' } + const a = address.trim() + if (!isAddress(a)) return { ok: false, reason: 'Not a valid EVM address' } + if (a.toLowerCase() === ZERO_ADDRESS) return { ok: false, reason: 'Zero/placeholder address' } + return { ok: true, checksum: getAddress(a) } +} + +function chainFor(network: PayoutNetwork) { + return network === 'base' ? base : baseSepolia +} + +function rpcFor(network: PayoutNetwork): string { + if (network === 'base') return process.env.BASE_RPC_URL?.trim() || 'https://mainnet.base.org' + return process.env.BASE_SEPOLIA_RPC_URL?.trim() || 'https://sepolia.base.org' +} + +/** Hot wallet status: address + USDC/ETH balances. null if no key configured. */ +export async function getHotWalletStatus(): Promise< + { address: Hex; network: PayoutNetwork; usdc: number; eth: string } | null +> { + const key = normalizeKey(process.env.PAYOUT_WALLET_PRIVATE_KEY) + if (!key) return null + const { network, usdcAddress } = getPayoutConfig() + const account = privateKeyToAccount(key) + const publicClient = createPublicClient({ chain: chainFor(network), transport: http(rpcFor(network)) }) + + const [usdcRaw, ethRaw] = await Promise.all([ + publicClient.readContract({ + address: usdcAddress, + abi: erc20Abi, + functionName: 'balanceOf', + args: [account.address], + }) as Promise, + publicClient.getBalance({ address: account.address }), + ]) + + return { + address: account.address, + network, + usdc: Number.parseFloat(formatUnits(usdcRaw, USDC_DECIMALS)), + eth: formatUnits(ethRaw, 18), + } +} + +export type SendResult = { txHash: string; dryRun: boolean } + +/** + * Send a single USDC payout. In dry-run mode returns a synthetic marker without + * touching the chain. Real mode waits for the receipt and throws if it reverts. + */ +export async function sendUsdc(to: string, amountUsd: number): Promise { + const { network, dryRun, usdcAddress } = getPayoutConfig() + + const v = validatePayoutAddress(to) + if (!v.ok || !v.checksum) throw new Error(`Invalid payout address: ${v.reason}`) + if (!(amountUsd > 0)) throw new Error('Payout amount must be > 0') + + if (dryRun) { + return { txHash: `DRYRUN:${network}:${v.checksum}:${amountUsd}`, dryRun: true } + } + + const key = normalizeKey(process.env.PAYOUT_WALLET_PRIVATE_KEY) + if (!key) throw new Error('PAYOUT_WALLET_PRIVATE_KEY not configured (cannot send live)') + + const account = privateKeyToAccount(key) + const chain = chainFor(network) + const rpc = rpcFor(network) + const walletClient = createWalletClient({ account, chain, transport: http(rpc) }) + const publicClient = createPublicClient({ chain, transport: http(rpc) }) + + // USDC has 6 decimals; commission amounts are rounded to cents upstream. + const value = parseUnits(amountUsd.toFixed(USDC_DECIMALS), USDC_DECIMALS) + + const hash = await walletClient.writeContract({ + address: usdcAddress, + abi: erc20Abi, + functionName: 'transfer', + args: [v.checksum, value], + }) + + const receipt = await publicClient.waitForTransactionReceipt({ hash }) + if (receipt.status !== 'success') throw new Error(`USDC transfer reverted (tx ${hash})`) + + return { txHash: hash, dryRun: false } +} From 8ea38d9dde891c2e662de7a50a0fa97b979e5e07 Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 21:37:36 -0700 Subject: [PATCH 02/46] feat(evm): single-switch Base mainnet/testnet network (ready to go) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded Base Sepolia across the runtime mint/transaction layer with one source of truth: lib/evm-network.ts. Going live = set NEXT_PUBLIC_EVM_NETWORK=base (+ the mainnet Genesis address). Defaults to base-sepolia so nothing hits mainnet by accident. evm-network.ts exposes the active chain, chainId, RPC, USDC token, explorer + OpenSea URL builders, network label, and wallet_addEthereumChain params. Refactored off hardcoded sepolia: - admin-genesis-mint.ts: server card-purchase mint now uses getEvmChain()/RPC (was chain: baseSepolia); network-aware misconfig messages. - GenesisMint.tsx: wallet switch/add-chain (0x14a34 → active), USDC token, explorer/OpenSea links, and copy now network-driven. - MagicProvider.tsx: embedded-wallet chainId/RPC from the switch (was 84532). - custodial/transfer + session-status: chain + explorer/OpenSea via the switch. - launch/page.tsx: Magic RPC prop uses getEvmRpcUrl() (was forced sepolia). - LaunchClient.tsx: explorer/OpenSea/labels via the switch. - Providers.tsx (wagmi): transports prefer configured RPCs. - .env.example: NEXT_PUBLIC_EVM_NETWORK + mainnet RPC/contract documented (mainnet Genesis 0x93c26c9B9fe702D7944aE62fc54e451d4470F21D). NOTE: makes the REPO mainnet-ready. The live site currently runs Dom's build via api.dagwelldev.com, so this is not yet the production path. Owner=Safe means the card-mint signer needs an authorized minter role (see admin-genesis-mint caveat). Co-Authored-By: Claude Opus 4.8 --- .env.example | 22 +++- .../app/api/checkout/session-status/route.ts | 5 +- .../src/app/api/custodial/transfer/route.ts | 13 +-- apps/web/src/app/launch/LaunchClient.tsx | 8 +- apps/web/src/app/launch/page.tsx | 7 +- apps/web/src/components/GenesisMint.tsx | 54 +++++---- apps/web/src/components/Providers.tsx | 7 +- .../src/components/magic/MagicProvider.tsx | 11 +- apps/web/src/lib/admin-genesis-mint.ts | 20 ++-- apps/web/src/lib/evm-network.ts | 105 ++++++++++++++++++ 10 files changed, 182 insertions(+), 70 deletions(-) create mode 100644 apps/web/src/lib/evm-network.ts diff --git a/.env.example b/.env.example index 07fd9d2..1e20f4b 100644 --- a/.env.example +++ b/.env.example @@ -11,8 +11,23 @@ KAFKA_BOOTSTRAP=localhost:9092 # Cardano (Blockfrost API for Pre-Prod) BLOCKFROST_PROJECT_ID=preprodYourProjectId -# EVM (Base Sepolia RPC) +# ── EVM network switch (lib/evm-network.ts) ────────────────────────────────── +# THE go-live switch. "base" = Base mainnet (real USDC/NFTs); "base-sepolia" = +# testnet (default). Governs chain, RPC, USDC token, explorer + OpenSea links, +# and which network the mint/transfer flows enforce in the wallet. +NEXT_PUBLIC_EVM_NETWORK=base-sepolia + +# RPCs (public defaults work but are rate-limited; set dedicated RPCs for prod). BASE_SEPOLIA_RPC_URL=https://sepolia.base.org +NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL=https://sepolia.base.org +# Used when NEXT_PUBLIC_EVM_NETWORK=base +BASE_RPC_URL=https://mainnet.base.org +NEXT_PUBLIC_BASE_RPC_URL=https://mainnet.base.org + +# Genesis ERC-721 for the ACTIVE network. Mainnet (Base 8453): +# 0x93c26c9B9fe702D7944aE62fc54e451d4470F21D +# Set this to match NEXT_PUBLIC_EVM_NETWORK (mainnet address when on "base"). +NEXT_PUBLIC_GENESIS_CONTRACT_ADDRESS=0x93c26c9B9fe702D7944aE62fc54e451d4470F21D # Contract deployment keys (use test keys only) AI_VALIDATOR_KEY=0x_your_test_private_key @@ -36,14 +51,13 @@ AUTH0_SECRET= # Optional API audience (resource identifier) for access tokens / Magic IDP alignment AUTH0_AUDIENCE= -# Genesis NFT (EVM) — used by web mint + card/custodial flow -NEXT_PUBLIC_GENESIS_CONTRACT_ADDRESS=0x_your_genesis_validator +# (Genesis contract address + Base RPCs are configured in the EVM network switch +# section near the top of this file — see NEXT_PUBLIC_EVM_NETWORK.) # Card checkout (Stripe) + custodial wallet — default `magic` (Magic Email OTP → embedded wallet). # Use `server` for legacy server-generated keys + transfer link only. MALAMA_CARD_CUSTODY=magic NEXT_PUBLIC_MAGIC_API_KEY=pk_live_or_test_your_magic_publishable_key -NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL=https://sepolia.base.org # Magic Secret API key (server) — validates DID tokens from /launch; same key family as Magic Express if applicable MAGIC_SECRET_KEY= diff --git a/apps/web/src/app/api/checkout/session-status/route.ts b/apps/web/src/app/api/checkout/session-status/route.ts index f46cf3c..b92f12a 100644 --- a/apps/web/src/app/api/checkout/session-status/route.ts +++ b/apps/web/src/app/api/checkout/session-status/route.ts @@ -6,6 +6,7 @@ import { getSessionStatus } from '@/lib/custodial-store' import { getStripeSecretKey } from '@/lib/stripe-server' import { requireGenesisContract } from '@/lib/genesis-contract' import { resolveAppUrl } from '@/lib/resolve-app-url' +import { getExplorerTxUrl, getOpenSeaAssetUrl } from '@/lib/evm-network' export const runtime = 'nodejs' @@ -42,8 +43,8 @@ export async function GET(req: Request) { custodialAddress: rec.address, evmTokenId: rec.evmTokenId, txHash: rec.txHash, - explorerUrl: `https://sepolia.basescan.org/tx/${rec.txHash}`, - openSeaUrl: `https://testnets.opensea.io/assets/base-sepolia/${GENESIS_CONTRACT}/${rec.evmTokenId}`, + explorerUrl: rec.txHash ? getExplorerTxUrl(rec.txHash) : undefined, + openSeaUrl: getOpenSeaAssetUrl(GENESIS_CONTRACT, rec.evmTokenId), transferUrl: rec.custody === 'server' ? `${appUrl}/custodial/transfer?claimId=${encodeURIComponent(rec.claimId)}&token=${encodeURIComponent(rec.transferToken)}` diff --git a/apps/web/src/app/api/custodial/transfer/route.ts b/apps/web/src/app/api/custodial/transfer/route.ts index 260fd22..55ce7d3 100644 --- a/apps/web/src/app/api/custodial/transfer/route.ts +++ b/apps/web/src/app/api/custodial/transfer/route.ts @@ -2,10 +2,10 @@ import { NextResponse } from 'next/server' import { isAddress } from 'viem' import { createWalletClient, http, parseAbi } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { baseSepolia } from 'viem/chains' import { getCustodialByClaimId } from '@/lib/custodial-store' import { decryptPrivateKeyHex } from '@/lib/wallet-crypto' import { requireGenesisContract } from '@/lib/genesis-contract' +import { getEvmChain, getEvmRpcUrl, getExplorerTxUrl } from '@/lib/evm-network' export const runtime = 'nodejs' @@ -55,15 +55,10 @@ export async function POST(req: Request) { return NextResponse.json({ error: 'Wallet mismatch' }, { status: 500 }) } - const rpc = process.env.BASE_SEPOLIA_RPC_URL || process.env.NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL - if (!rpc) { - return NextResponse.json({ error: 'RPC not configured' }, { status: 503 }) - } - const walletClient = createWalletClient({ account, - chain: baseSepolia, - transport: http(rpc), + chain: getEvmChain(), + transport: http(getEvmRpcUrl()), }) const hash = await walletClient.writeContract({ @@ -76,7 +71,7 @@ export async function POST(req: Request) { return NextResponse.json({ success: true, txHash: hash, - explorerUrl: `https://sepolia.basescan.org/tx/${hash}`, + explorerUrl: getExplorerTxUrl(hash), }) } catch (e) { console.error('[custodial/transfer]', e) diff --git a/apps/web/src/app/launch/LaunchClient.tsx b/apps/web/src/app/launch/LaunchClient.tsx index 29b0df5..7698d26 100644 --- a/apps/web/src/app/launch/LaunchClient.tsx +++ b/apps/web/src/app/launch/LaunchClient.tsx @@ -5,6 +5,7 @@ import Link from 'next/link' import { useCallback, useEffect, useState } from 'react' import { Loader2, Wallet, ExternalLink, CheckCircle2, AlertCircle } from 'lucide-react' import { useMagic } from '@/components/magic/MagicProvider' +import { getExplorerTxUrl, getOpenSeaAssetUrl, getNetworkLabel } from '@/lib/evm-network' const GENESIS_CONTRACT = process.env.NEXT_PUBLIC_GENESIS_CONTRACT_ADDRESS ?? '' @@ -116,14 +117,13 @@ export default function LaunchClient({ hasMagicPublishableKey }: { hasMagicPubli if (claimResult) { const openSea = - GENESIS_CONTRACT && - `https://testnets.opensea.io/assets/base-sepolia/${GENESIS_CONTRACT}/${claimResult.evmTokenId}` + GENESIS_CONTRACT && getOpenSeaAssetUrl(GENESIS_CONTRACT, claimResult.evmTokenId) return (

{claimResult.claimId}

- Your Genesis NFT is in your Magic wallet on Base Sepolia. You can connect this app or any wallet UI that + Your Genesis NFT is in your Magic wallet on {getNetworkLabel()}. You can connect this app or any wallet UI that supports Magic to manage it.

@@ -138,7 +138,7 @@ export default function LaunchClient({ hasMagicPublishableKey }: { hasMagicPubli
diff --git a/apps/web/src/components/GenesisMint.tsx b/apps/web/src/components/GenesisMint.tsx index 082bcab..6d7a7b6 100644 --- a/apps/web/src/components/GenesisMint.tsx +++ b/apps/web/src/components/GenesisMint.tsx @@ -27,14 +27,24 @@ import { // ─── Contract addresses ─────────────────────────────────────────────────────── import { tryGetGenesisContract, GENESIS_CONTRACT_PLACEHOLDER } from '@/lib/genesis-contract' +import { + getUsdcAddress, + getEvmChainId, + getAddEthereumChainParams, + getNetworkLabel, + getExplorerTxUrl, + getOpenSeaAssetUrl, +} from '@/lib/evm-network' // Module-level fallback to placeholder so this client bundle builds + renders // even when the env var isn't set on a preview branch. The actual mint flow // (handleBasePayment) guards against the placeholder and surfaces a clear // error — so previews don't silently mint to a non-existent contract. const GENESIS_CONTRACT = (tryGetGenesisContract() ?? GENESIS_CONTRACT_PLACEHOLDER) as `0x${string}` -// Real USDC on Base Sepolia. Override with NEXT_PUBLIC_MOCK_USDC_ADDRESS for local testing. -const USDC_CONTRACT = (process.env.NEXT_PUBLIC_MOCK_USDC_ADDRESS ?? '0x036CbD53842c5426634e7929541eC2318f3dCF7e') as `0x${string}` +// USDC for the active network (lib/evm-network). Override with NEXT_PUBLIC_MOCK_USDC_ADDRESS for local testing. +const USDC_CONTRACT = (process.env.NEXT_PUBLIC_MOCK_USDC_ADDRESS ?? getUsdcAddress()) as `0x${string}` const PRICE_USDC = parseUnits('2000', 6) // $2,000 USDC (6 decimals) +// Active chain id as 0x-hex for wallet_switch/addEthereumChain. +const ACTIVE_CHAIN_HEX = `0x${getEvmChainId().toString(16)}` const USDC_ABI = parseAbi([ 'function approve(address spender, uint256 amount) public returns (bool)', @@ -206,23 +216,17 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) { if (!eth) return // panel is open; user reads contract + tokenId manually try { - // Switch to Base Sepolia first + // Switch to the active network first const chainId: string = await eth.request({ method: 'eth_chainId' }) - if (chainId !== '0x14a34') { + if (chainId !== ACTIVE_CHAIN_HEX) { await eth.request({ method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x14a34' }], + params: [{ chainId: ACTIVE_CHAIN_HEX }], }).catch(async (switchErr: any) => { if (switchErr.code === 4902) { await eth.request({ method: 'wallet_addEthereumChain', - params: [{ - chainId: '0x14a34', - chainName: 'Base Sepolia', - nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, - rpcUrls: ['https://sepolia.base.org'], - blockExplorerUrls: ['https://sepolia.basescan.org'], - }], + params: [getAddEthereumChainParams()], }) } }) @@ -257,27 +261,21 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) { ) } - // 0. Enforce Base Sepolia — switch if needed + // 0. Enforce the active network — switch if needed const eth = (window as any).ethereum if (eth) { const currentChain: string = await eth.request({ method: 'eth_chainId' }) - if (currentChain !== '0x14a34') { + if (currentChain !== ACTIVE_CHAIN_HEX) { try { - await eth.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: '0x14a34' }] }) + await eth.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: ACTIVE_CHAIN_HEX }] }) } catch (switchErr: any) { if (switchErr.code === 4902) { await eth.request({ method: 'wallet_addEthereumChain', - params: [{ - chainId: '0x14a34', - chainName: 'Base Sepolia', - nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, - rpcUrls: ['https://sepolia.base.org'], - blockExplorerUrls: ['https://sepolia.basescan.org'], - }], + params: [getAddEthereumChainParams()], }) } else { - throw new Error('Please switch to Base Sepolia in your wallet') + throw new Error(`Please switch to ${getNetworkLabel()} in your wallet`) } } } @@ -364,8 +362,8 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) { evmTokenId, txHash: mintHash, chain: 'base' as const, - explorerUrl: `https://sepolia.basescan.org/tx/${mintHash}`, - openSeaUrl: `https://testnets.opensea.io/assets/base-sepolia/${GENESIS_CONTRACT}/${evmTokenId}`, + explorerUrl: getExplorerTxUrl(mintHash), + openSeaUrl: getOpenSeaAssetUrl(GENESIS_CONTRACT, evmTokenId), nftImageUrl: `${appBase}/api/nft/${evmTokenId}/image?hexId=${encodeURIComponent(hexId)}&chain=base&claimId=${encodeURIComponent(claimId)}`, } } @@ -582,7 +580,7 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) {

Crypto: connect Cardano (Lace) and/or Base (MetaMask). Card: pay with Stripe, then open Launch App and - sign in with Magic using the same email. Your NFT mints to your embedded wallet on Base Sepolia. + sign in with Magic using the same email. Your NFT mints to your embedded wallet on {getNetworkLabel()}. Entry is $2,000 USDC or card checkout.

{hexId && ( @@ -911,7 +909,7 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) {

{paymentMode === 'card' - ? 'You will be redirected to Stripe Checkout. After payment clears, open Launch App, sign in with Magic (same email), and we mint your Genesis NFT to your embedded wallet on Base Sepolia.' + ? `You will be redirected to Stripe Checkout. After payment clears, open Launch App, sign in with Magic (same email), and we mint your Genesis NFT to your embedded wallet on ${getNetworkLabel()}.` : evmConnected ? 'Your wallet will prompt you to approve $2,000 USDC and then sign the mint transaction on Base.' : 'The server will mint your Cardano CIP-25 NFT directly to your wallet. No gas required from you.'} @@ -1060,7 +1058,7 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) {

Network

-

Base Sepolia (chain 84532)

+

{getNetworkLabel()} (chain {getEvmChainId()})

Contract Address

diff --git a/apps/web/src/components/Providers.tsx b/apps/web/src/components/Providers.tsx index e86df5e..c473592 100644 --- a/apps/web/src/components/Providers.tsx +++ b/apps/web/src/components/Providers.tsx @@ -6,12 +6,15 @@ import { WagmiProvider, createConfig, http } from "wagmi"; import { base, baseSepolia } from "wagmi/chains"; import { injected, coinbaseWallet } from "wagmi/connectors"; +// Both chains stay supported; transports prefer configured RPCs (public defaults +// are rate-limited). The active network is governed by lib/evm-network.ts and the +// mint components, which switch the wallet to the right chain explicitly. const wagmiConfig = createConfig({ chains: [base, baseSepolia], connectors: [injected(), coinbaseWallet({ appName: "Mālama Labs" })], transports: { - [base.id]: http(), - [baseSepolia.id]: http(), + [base.id]: http(process.env.NEXT_PUBLIC_BASE_RPC_URL?.trim() || undefined), + [baseSepolia.id]: http(process.env.NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL?.trim() || undefined), }, }); diff --git a/apps/web/src/components/magic/MagicProvider.tsx b/apps/web/src/components/magic/MagicProvider.tsx index 9f4edb2..c25c01f 100644 --- a/apps/web/src/components/magic/MagicProvider.tsx +++ b/apps/web/src/components/magic/MagicProvider.tsx @@ -2,6 +2,7 @@ import { Magic } from 'magic-sdk' import { type ReactNode, createContext, useContext, useEffect, useMemo, useState } from 'react' +import { getEvmChainId, getEvmRpcUrl } from '@/lib/evm-network' type MagicContextType = { magic: InstanceType | null } @@ -11,8 +12,6 @@ export function useMagic() { return useContext(MagicContext) } -const BASE_SEPOLIA_CHAIN_ID = 84532 - type Props = { children: ReactNode /** Prefer passing from a Server Component so the key is read from apps/web/.env.local at request time. */ @@ -27,17 +26,13 @@ export function MagicProvider({ children, publishableKey, rpcUrl }: Props) { const key = publishableKey?.trim() || process.env.NEXT_PUBLIC_MAGIC_API_KEY?.trim() - const rpc = - rpcUrl?.trim() || - process.env.NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL?.trim() || - process.env.BASE_SEPOLIA_RPC_URL?.trim() || - 'https://sepolia.base.org' + const rpc = rpcUrl?.trim() || getEvmRpcUrl() if (!key) return const instance = new Magic(key, { network: { rpcUrl: rpc, - chainId: BASE_SEPOLIA_CHAIN_ID, + chainId: getEvmChainId(), }, }) setMagic(instance) diff --git a/apps/web/src/lib/admin-genesis-mint.ts b/apps/web/src/lib/admin-genesis-mint.ts index 0cdf99a..d5208ee 100644 --- a/apps/web/src/lib/admin-genesis-mint.ts +++ b/apps/web/src/lib/admin-genesis-mint.ts @@ -6,7 +6,7 @@ import { parseAbi, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { baseSepolia } from 'viem/chains' +import { getEvmChain, getEvmRpcUrl, isMainnet } from '@/lib/evm-network' const MHNL_ABI = parseAbi([ 'function adminSecureNode(address to, string calldata hexId) external', @@ -16,8 +16,11 @@ const MHNL_ABI = parseAbi([ const GENESIS_CONTRACT = (process.env.NEXT_PUBLIC_GENESIS_CONTRACT_ADDRESS ?? '0x2222222222222222222222222222222222222222') as `0x${string}` +/** Raw configured RPC for the active network (undefined if not explicitly set). */ function getRpc() { - return process.env.BASE_SEPOLIA_RPC_URL || process.env.NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL + return isMainnet() + ? process.env.NEXT_PUBLIC_BASE_RPC_URL?.trim() || process.env.BASE_RPC_URL?.trim() + : process.env.BASE_SEPOLIA_RPC_URL?.trim() || process.env.NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL?.trim() } export async function adminMintToAddress(opts: { @@ -32,7 +35,7 @@ export async function adminMintToAddress(opts: { if (!ownerKey || !rpc || isPlaceholderContract) { const missing = [ !ownerKey && 'GENESIS_OWNER_PRIVATE_KEY', - !rpc && 'BASE_SEPOLIA_RPC_URL', + !rpc && (isMainnet() ? 'BASE_RPC_URL' : 'BASE_SEPOLIA_RPC_URL'), isPlaceholderContract && 'NEXT_PUBLIC_GENESIS_CONTRACT_ADDRESS', ].filter(Boolean).join(', ') throw new Error(`CRITICAL: Production minting requires valid configuration but env is missing: ${missing}. Simulation is strictly forbidden.`) @@ -43,7 +46,7 @@ export async function adminMintToAddress(opts: { if (process.env.MINT_SIMULATION !== 'true') { const missing = [ !ownerKey && 'GENESIS_OWNER_PRIVATE_KEY', - !rpc && 'BASE_SEPOLIA_RPC_URL', + !rpc && (isMainnet() ? 'BASE_RPC_URL' : 'BASE_SEPOLIA_RPC_URL'), isPlaceholderContract && 'NEXT_PUBLIC_GENESIS_CONTRACT_ADDRESS', ].filter(Boolean).join(', ') throw new Error(`Mint misconfigured (missing: ${missing}). Set MINT_SIMULATION=true to simulate.`) @@ -60,8 +63,8 @@ export async function adminMintToAddress(opts: { console.log('[admin-mint] signer', account.address) const walletClient = createWalletClient({ account, - chain: baseSepolia, - transport: http(rpc), + chain: getEvmChain(), + transport: http(getEvmRpcUrl()), }) const hash = await walletClient.writeContract({ @@ -77,9 +80,8 @@ export async function adminMintToAddress(opts: { export async function resolveTokenIdFromTx( hash: `0x${string}`, ): Promise { - const rpc = getRpc() - if (!rpc || hash.startsWith('0xmock_')) return null - const publicClient = createPublicClient({ chain: baseSepolia, transport: http(rpc) }) + if (hash.startsWith('0xmock_')) return null + const publicClient = createPublicClient({ chain: getEvmChain(), transport: http(getEvmRpcUrl()) }) try { const receipt = await publicClient.waitForTransactionReceipt({ hash, timeout: 30_000 }) for (const log of receipt.logs) { diff --git a/apps/web/src/lib/evm-network.ts b/apps/web/src/lib/evm-network.ts new file mode 100644 index 0000000..acf79ba --- /dev/null +++ b/apps/web/src/lib/evm-network.ts @@ -0,0 +1,105 @@ +/** + * Single source of truth for the active EVM network (Base mainnet vs Base Sepolia). + * + * GO LIVE BY FLIPPING ONE ENV VAR: NEXT_PUBLIC_EVM_NETWORK=base + * Defaults to `base-sepolia` (testnet) until explicitly set to `base`, so nothing + * touches mainnet by accident. + * + * Every component/route that needs a chain, RPC, USDC token, Genesis contract, + * or explorer/marketplace URL should import from here — never hardcode a network. + * + * Env: + * NEXT_PUBLIC_EVM_NETWORK "base" (mainnet) | "base-sepolia" (default) + * NEXT_PUBLIC_GENESIS_CONTRACT_ADDRESS Genesis ERC-721 for the ACTIVE network + * NEXT_PUBLIC_BASE_RPC_URL / BASE_RPC_URL mainnet RPC + * NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL / BASE_SEPOLIA_RPC_URL testnet RPC + */ + +import { base, baseSepolia, type Chain } from 'viem/chains' + +export type EvmNetwork = 'base' | 'base-sepolia' + +export function getEvmNetwork(): EvmNetwork { + return process.env.NEXT_PUBLIC_EVM_NETWORK?.trim() === 'base' ? 'base' : 'base-sepolia' +} + +export function isMainnet(): boolean { + return getEvmNetwork() === 'base' +} + +const CHAIN_ID: Record = { base: 8453, 'base-sepolia': 84532 } + +// Canonical USDC token (6 decimals) per network. +const USDC: Record = { + base: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + 'base-sepolia': '0x036CbD53842c5426634e7929541eC2318f3dCF7e', +} + +const EXPLORER: Record = { + base: 'https://basescan.org', + 'base-sepolia': 'https://sepolia.basescan.org', +} + +const OPENSEA: Record = { + base: 'https://opensea.io/assets/base', + 'base-sepolia': 'https://testnets.opensea.io/assets/base-sepolia', +} + +export function getEvmChain(): Chain { + return isMainnet() ? base : baseSepolia +} + +export function getEvmChainId(): number { + return CHAIN_ID[getEvmNetwork()] +} + +export function getUsdcAddress(): `0x${string}` { + return USDC[getEvmNetwork()] +} + +/** RPC URL for the active network (public var preferred, then server var, then public default). */ +export function getEvmRpcUrl(): string { + if (isMainnet()) { + return ( + process.env.NEXT_PUBLIC_BASE_RPC_URL?.trim() || + process.env.BASE_RPC_URL?.trim() || + 'https://mainnet.base.org' + ) + } + return ( + process.env.NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL?.trim() || + process.env.BASE_SEPOLIA_RPC_URL?.trim() || + 'https://sepolia.base.org' + ) +} + +export function getNetworkLabel(): string { + return isMainnet() ? 'Base' : 'Base Sepolia' +} + +export function getExplorerBase(): string { + return EXPLORER[getEvmNetwork()] +} + +export function getExplorerTxUrl(hash: string): string { + return `${getExplorerBase()}/tx/${hash}` +} + +export function getExplorerAddressUrl(addr: string): string { + return `${getExplorerBase()}/address/${addr}` +} + +export function getOpenSeaAssetUrl(contract: string, tokenId: string | number): string { + return `${OPENSEA[getEvmNetwork()]}/${contract}/${tokenId}` +} + +/** wallet_addEthereumChain params for MetaMask-style add/switch flows. */ +export function getAddEthereumChainParams() { + return { + chainId: `0x${getEvmChainId().toString(16)}`, + chainName: getNetworkLabel(), + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: [getEvmRpcUrl()], + blockExplorerUrls: [getExplorerBase()], + } +} From 7ea07ab024f087b1265f08e1cf7c11dc2aa7d195 Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 21:44:30 -0700 Subject: [PATCH 03/46] =?UTF-8?q?feat(checkout):=20cheap-test=20support=20?= =?UTF-8?q?=E2=80=94=20$1=20env=20override=20+=20promo=20codes=20+=20metad?= =?UTF-8?q?ata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CHECKOUT_TEST_PRICE_CENTS env override (e.g. 100=$1) for whole-env testing; clamped to [50, 200000], defaults to real price, logs loudly when active. - allow_promotion_codes: true so a Stripe coupon ($1999 off → $1) enables SELECTIVE live testing without changing the price for real buyers. - Session metadata now carries unitAmountCents and testCheckout=true when a test price is in effect, so test purchases are filterable in Stripe + fulfillment. Co-Authored-By: Claude Opus 4.8 --- .../app/api/checkout/create-session/route.ts | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/api/checkout/create-session/route.ts b/apps/web/src/app/api/checkout/create-session/route.ts index b563358..4ddf123 100644 --- a/apps/web/src/app/api/checkout/create-session/route.ts +++ b/apps/web/src/app/api/checkout/create-session/route.ts @@ -16,6 +16,30 @@ export const runtime = 'nodejs' const PRICE_CENTS = 200_000 // $2,000.00 +/** + * Resolve checkout amount in cents. + * + * For safe end-to-end testing without a $2,000 charge, set CHECKOUT_TEST_PRICE_CENTS + * (e.g. 100 = $1.00) in a test/staging env. Defaults to the real price; clamped to + * [50 (Stripe USD min), PRICE_CENTS] so it can never exceed the real price; logs a + * loud warning when active. MUST be unset in production. + * + * For SELECTIVE testing on a live/prod env, prefer a Stripe promo code instead + * (allow_promotion_codes is enabled below) — create a coupon for $1999 off in the + * Stripe dashboard so a tester pays $1 while real buyers pay full price. + */ +function resolveUnitAmount(): number { + const raw = process.env.CHECKOUT_TEST_PRICE_CENTS + if (!raw) return PRICE_CENTS + const cents = Number.parseInt(raw, 10) + if (!Number.isFinite(cents) || cents < 50 || cents > PRICE_CENTS) return PRICE_CENTS + console.warn( + `[checkout/create-session] ⚠️ TEST PRICE OVERRIDE active: $${(cents / 100).toFixed(2)} ` + + `(CHECKOUT_TEST_PRICE_CENTS=${cents}). Unset in production.` + ) + return cents +} + export async function POST(req: Request) { try { const secret = getStripeSecretKey() @@ -58,14 +82,20 @@ export async function POST(req: Request) { const stripe = new Stripe(secret) + const unitAmount = resolveUnitAmount() + const isTestPrice = unitAmount !== PRICE_CENTS + const session = await stripe.checkout.sessions.create({ mode: 'payment', customer_email: emailNorm, + // Let testers apply a Stripe promo code (e.g. a $1999-off coupon → $1) for + // selective live testing without changing the price for real buyers. + allow_promotion_codes: true, line_items: [ { price_data: { currency: 'usd', - unit_amount: PRICE_CENTS, + unit_amount: unitAmount, product_data: { name: 'Mālama Genesis Hex Node License', description: `H3 territory: ${hexId.slice(0, 18)}…`, @@ -80,6 +110,8 @@ export async function POST(req: Request) { hexId, email: emailNorm, transferToken, + unitAmountCents: String(unitAmount), + ...(isTestPrice ? { testCheckout: 'true' } : {}), ...(referrerId ? { referrerId } : {}), }, }) From 32281cb7a73b99c32f7d12097267379124f22e0b Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 21:44:30 -0700 Subject: [PATCH 04/46] fix(presale): labeled progress stepper bound to real wizard step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the unlabeled progress bars with a numbered + labeled stepper (1 Locate Hex · 2 Crypto or Card · 3 Review · 4 Pay · 5 Reserved) driven by the existing `step` state: current step highlighted, completed steps checked, upcoming muted. Labels collapse to numbers on mobile. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/components/GenesisMint.tsx | 45 ++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/GenesisMint.tsx b/apps/web/src/components/GenesisMint.tsx index 6d7a7b6..43706a5 100644 --- a/apps/web/src/components/GenesisMint.tsx +++ b/apps/web/src/components/GenesisMint.tsx @@ -499,11 +499,46 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) { // ───────────────────────────────────────────────────────────────────────── return (
- {/* Progress: 1 Locate HEX · 2 Crypto/Card · 3 Review · 4 Pay · 5 Done */} -
- {[1, 2, 3, 4, 5].map((s) => ( -
- ))} + {/* Progress stepper — bound to the live wizard step (1..5). */} +
+ {[ + { n: 1, label: 'Locate Hex' }, + { n: 2, label: 'Crypto or Card' }, + { n: 3, label: 'Review' }, + { n: 4, label: 'Pay' }, + { n: 5, label: 'Reserved' }, + ].map((it, i) => { + const done = it.n < step + const current = it.n === step + return ( + +
+
+ {done ? '✓' : it.n} +
+ + {it.label} + +
+ {i < 4 && ( +
+ )} + + ) + })}
From 1a47dd337882ad306eae0b0e3fcc475605f284fd Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 21:48:47 -0700 Subject: [PATCH 05/46] =?UTF-8?q?feat(dashboard):=20post-ownership=20hardw?= =?UTF-8?q?are=20status=20=E2=80=94=20order/customization=20underway?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owned-node status now reads "Hardware Order & Customization Underway" with "Shipment expected by Dec 31, 2026" (was the misleading "Hardware Shipped / In Transit - Expected in 6 Months"). Co-Authored-By: Claude Opus 4.8 --- apps/web/src/app/dashboard/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/dashboard/page.tsx b/apps/web/src/app/dashboard/page.tsx index 894b054..d265d29 100644 --- a/apps/web/src/app/dashboard/page.tsx +++ b/apps/web/src/app/dashboard/page.tsx @@ -401,7 +401,7 @@ export default function Dashboard() { } // ───────────────────────────────────────────────────────────────────────── - const currentStatus = hexes.length > 0 ? 'Hardware Pending' : 'Awaiting Genesis License' + const currentStatus = hexes.length > 0 ? 'Hardware Order & Customization Underway' : 'Awaiting Genesis License' const activePredictionMarkets = hexes.length > 0 ? 8 : 0 useEffect(() => { @@ -717,10 +717,10 @@ export default function Dashboard() { 0 ? 'text-malama-teal' : 'text-gray-500'}`} />
0 ? 'text-malama-teal' : 'text-gray-500'}`}> - Hardware Shipped + Hardware Underway - {hexes.length > 0 ? 'In Transit - Expected in 6 Months' : 'Pending Verification'} + {hexes.length > 0 ? 'Order & customization underway · Shipment expected by Dec 31, 2026' : 'Pending Verification'}
From 0cfb3e6f68c0cc09f9bbd8f530e68cf12002a4c5 Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 21:51:27 -0700 Subject: [PATCH 06/46] chore: add .claude/settings.json allowlist to canonical branch Consolidating onto ready-to-go: bring the permission allowlist (npm/pnpm/ hardhat/aiken/pytest/git read+commit; denies rm -rf, force-push, hard reset) onto the canonical branch so it's complete in one place. Co-Authored-By: Claude Opus 4.8 --- .claude/settings.json | 51 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..b4d690d --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,51 @@ +{ + "permissions": { + "allow": [ + "Bash(npm install:*)", + "Bash(npm ci:*)", + "Bash(npm run:*)", + "Bash(npm test:*)", + "Bash(pnpm install:*)", + "Bash(pnpm run:*)", + "Bash(pnpm test:*)", + "Bash(pnpm lint:*)", + "Bash(pnpm build:*)", + "Bash(pnpm dev:*)", + "Bash(pnpm typecheck:*)", + "Bash(npx hardhat:*)", + "Bash(aiken:*)", + "Bash(pytest:*)", + "Bash(python -m pytest:*)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)", + "Bash(git show:*)", + "Bash(git branch:*)", + "Bash(git fetch:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git checkout:*)", + "Bash(git switch:*)", + "Bash(git restore:*)", + "Bash(git stash:*)", + "Bash(ls:*)", + "Bash(cat:*)", + "Bash(rg:*)", + "Bash(grep:*)", + "Bash(find:*)", + "Bash(head:*)", + "Bash(tail:*)", + "Bash(wc:*)", + "Bash(tree:*)", + "Bash(echo:*)", + "Bash(which:*)", + "Bash(pwd)" + ], + "deny": [ + "Bash(rm -rf:*)", + "Bash(git push --force:*)", + "Bash(git push -f:*)", + "Bash(git reset --hard:*)" + ] + } +} From 58ff4c054717e8a3a7fcc4e6c70e8d5fb8a03389 Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 21:58:28 -0700 Subject: [PATCH 07/46] =?UTF-8?q?fix(presale):=20re-entrancy=20guard=20on?= =?UTF-8?q?=20pay=20flow=20=E2=80=94=20stop=20/api/nft/claim=20409=20spam?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pay handler was gated only by async `loading` state; a rapid re-trigger before it commits could re-enter and re-POST /api/nft/claim, spamming 409s (hex already claimed) in the console. Add a synchronous useRef lock so the pay flow can never run concurrently. The 409 was already handled as a resume (non-fatal), so this removes the redundant requests + console noise. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/components/GenesisMint.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/GenesisMint.tsx b/apps/web/src/components/GenesisMint.tsx index 43706a5..e41f0f1 100644 --- a/apps/web/src/components/GenesisMint.tsx +++ b/apps/web/src/components/GenesisMint.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useState } from 'react' +import React, { useState, useRef } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { useWallet as useCardanoWallet } from '@meshsdk/react' import { @@ -111,6 +111,11 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) { const [cardEmail, setCardEmail] = useState('') const [mmImportOpen, setMmImportOpen] = useState(false) const [mmCopied, setMmCopied] = useState<'address' | 'tokenId' | null>(null) + // Synchronous re-entrancy lock for the pay flow. React's `loading` state is + // async, so a rapid double-trigger (or wallet re-fire) can slip through before + // it commits — that's what spams /api/nft/claim with repeated 409s. A ref flips + // immediately and blocks any concurrent run. + const payInFlightRef = useRef(false) const legalComplete = allLegalAcknowledged(legalAck) @@ -468,6 +473,8 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) { // ── Main payment dispatcher ─────────────────────────────────────────────── const handlePayment = async () => { + if (payInFlightRef.current) return // re-entrancy guard — blocks duplicate claim POSTs / 409 spam + payInFlightRef.current = true setLoading(true) setError('') try { @@ -479,6 +486,7 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) { } catch (err: any) { setError(err.message ?? 'Payment failed. Please try again') } finally { + payInFlightRef.current = false setLoading(false) setEvmTxStatus('') } From 38d8dbfe90c9aa84c6c4f33e972a8124dd58fa40 Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 22:00:53 -0700 Subject: [PATCH 08/46] feat(brand): favicon + SVG logo, and User Docs footer link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add app/favicon.ico (16/32/48 multi-size), app/icon.png, app/apple-icon.png generated from the official mark — fixes the /favicon.ico 404 (Next App Router auto-serves + links these). - Add public/logo.svg (brand SVG logo) for in-app use. - Footer: add "User Docs" → https://docs.malamalabs.com (external) in the Documentation column. Co-Authored-By: Claude Opus 4.8 --- apps/web/public/logo.svg | 9 +++++++++ apps/web/src/app/favicon.ico | Bin 0 -> 15086 bytes apps/web/src/components/SiteFooter.tsx | 1 + 3 files changed, 10 insertions(+) create mode 100644 apps/web/public/logo.svg create mode 100644 apps/web/src/app/favicon.ico diff --git a/apps/web/public/logo.svg b/apps/web/public/logo.svg new file mode 100644 index 0000000..3e5d19a --- /dev/null +++ b/apps/web/public/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/src/app/favicon.ico b/apps/web/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..02d6810fefce1b948bfed2ec51ac3d5bd53a248b GIT binary patch literal 15086 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 jHOG4lSj2vy{f9vBc=La4zH1T~F9{@{16{kFU Date: Sun, 7 Jun 2026 22:01:15 -0700 Subject: [PATCH 09/46] feat(brand): add app icon.png + apple-icon.png (force past .gitignore *.png) These are intentional Next App Router icon assets; the repo's broad *.png ignore was excluding them. favicon.ico already shipped in the prior commit. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/app/apple-icon.png | Bin 0 -> 84786 bytes apps/web/src/app/icon.png | Bin 0 -> 3060 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/web/src/app/apple-icon.png create mode 100644 apps/web/src/app/icon.png diff --git a/apps/web/src/app/apple-icon.png b/apps/web/src/app/apple-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..87f8ada3e78ea94b77c8ee8d8e0c618524bcad0a GIT binary patch literal 84786 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%0AHkyqI)D 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 literal 0 HcmV?d00001 From bda8c9de9bd1129c73134ce5fce503e34de45cef Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 22:02:38 -0700 Subject: [PATCH 10/46] feat(footer): trust-anchor tagline + social links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update footer brand tagline to the two-product-line messaging (carbon dMRV + AI compute monitoring, one signing architecture). Add LinkedIn, YouTube, Medium, and Twitter/X icon links (currentColor → matches theme, open in new tab). Co-Authored-By: Claude Opus 4.8 --- apps/web/src/components/SiteFooter.tsx | 42 +++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/SiteFooter.tsx b/apps/web/src/components/SiteFooter.tsx index d0d6e48..ea14a46 100644 --- a/apps/web/src/components/SiteFooter.tsx +++ b/apps/web/src/components/SiteFooter.tsx @@ -49,7 +49,9 @@ export default function SiteFooter() { {footerSections.map((section) => (
From 2b0378585ba5afb4e50e4d6239c80523517280b7 Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 22:05:02 -0700 Subject: [PATCH 11/46] fix(copy): say "Base" in mint prose instead of network label (no "Sepolia") User-facing copy (Step 2, card-pay step, Launch success) hardcodes "Base" rather than getNetworkLabel(), which renders "Base Sepolia" while the env defaults to testnet. The actual chain switch stays dynamic via lib/evm-network. The technical network chip + wallet-switch error keep the accurate label. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/app/launch/LaunchClient.tsx | 4 ++-- apps/web/src/components/GenesisMint.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/launch/LaunchClient.tsx b/apps/web/src/app/launch/LaunchClient.tsx index 7698d26..3de0594 100644 --- a/apps/web/src/app/launch/LaunchClient.tsx +++ b/apps/web/src/app/launch/LaunchClient.tsx @@ -5,7 +5,7 @@ import Link from 'next/link' import { useCallback, useEffect, useState } from 'react' import { Loader2, Wallet, ExternalLink, CheckCircle2, AlertCircle } from 'lucide-react' import { useMagic } from '@/components/magic/MagicProvider' -import { getExplorerTxUrl, getOpenSeaAssetUrl, getNetworkLabel } from '@/lib/evm-network' +import { getExplorerTxUrl, getOpenSeaAssetUrl } from '@/lib/evm-network' const GENESIS_CONTRACT = process.env.NEXT_PUBLIC_GENESIS_CONTRACT_ADDRESS ?? '' @@ -123,7 +123,7 @@ export default function LaunchClient({ hasMagicPublishableKey }: { hasMagicPubli

{claimResult.claimId}

- Your Genesis NFT is in your Magic wallet on {getNetworkLabel()}. You can connect this app or any wallet UI that + Your Genesis NFT is in your Magic wallet on Base. You can connect this app or any wallet UI that supports Magic to manage it.

diff --git a/apps/web/src/components/GenesisMint.tsx b/apps/web/src/components/GenesisMint.tsx index e41f0f1..e979255 100644 --- a/apps/web/src/components/GenesisMint.tsx +++ b/apps/web/src/components/GenesisMint.tsx @@ -623,7 +623,7 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) {

Crypto: connect Cardano (Lace) and/or Base (MetaMask). Card: pay with Stripe, then open Launch App and - sign in with Magic using the same email. Your NFT mints to your embedded wallet on {getNetworkLabel()}. + sign in with Magic using the same email. Your NFT mints to your embedded wallet on Base. Entry is $2,000 USDC or card checkout.

{hexId && ( @@ -952,7 +952,7 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) {

{paymentMode === 'card' - ? `You will be redirected to Stripe Checkout. After payment clears, open Launch App, sign in with Magic (same email), and we mint your Genesis NFT to your embedded wallet on ${getNetworkLabel()}.` + ? 'You will be redirected to Stripe Checkout. After payment clears, open Launch App, sign in with Magic (same email), and we mint your Genesis NFT to your embedded wallet on Base.' : evmConnected ? 'Your wallet will prompt you to approve $2,000 USDC and then sign the mint transaction on Base.' : 'The server will mint your Cardano CIP-25 NFT directly to your wallet. No gas required from you.'} From c1c9eb14b9d576a1fa0e85e52d24c7582d8c9f6e Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 22:24:51 -0700 Subject: [PATCH 12/46] feat(partners): account-linked applications + Amplify push toolkit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply form: - Fix "Tyler Malin" default → "Your name" placeholder. - Prefill email + Base wallet from the signed-in account; link the application to that account (userId) when an email session exists. - Capture Telegram / LinkedIn / Reddit handles alongside X. Model: KOLPartner gains telegram, linkedin, reddit, promoMethod, userId (all persisted via the apply route; registerKOL already spreads input). Amplify (lib/amplify.ts): channel-specific ready-to-post copy with the partner's referral link baked in + one-click share intents — X (tweet composer), Reddit (link-post into r/depin + suggested DePIN subs), LinkedIn (share-offsite), Telegram (share sheet), Discord (copy/paste). Approved partner dashboard: new "Amplify — push your link" section rendering all channels with Share + Copy actions, subreddit chips, and per-sale earnings nudge to drive organic distribution. Follow-up (next): admin UI to edit channel messaging/links + view all partner activity (admin already has stats + 4 templates to build on). Co-Authored-By: Claude Opus 4.8 --- apps/web/src/app/api/partners/apply/route.ts | 28 ++++- apps/web/src/app/partners/apply/page.tsx | 61 +++++++++- apps/web/src/app/partners/dashboard/page.tsx | 59 +++++++++- apps/web/src/lib/amplify.ts | 114 +++++++++++++++++++ apps/web/src/lib/kol-registry.ts | 8 ++ 5 files changed, 263 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/lib/amplify.ts diff --git a/apps/web/src/app/api/partners/apply/route.ts b/apps/web/src/app/api/partners/apply/route.ts index 48294d8..cb06180 100644 --- a/apps/web/src/app/api/partners/apply/route.ts +++ b/apps/web/src/app/api/partners/apply/route.ts @@ -8,10 +8,19 @@ */ import { NextResponse } from 'next/server' +import { cookies } from 'next/headers' import { registerKOL, getKOLByWallet, buildReferralUrl, buildVanityUrl } from '@/lib/kol-registry' +import { parseEmailSessionToken } from '@/lib/email-session' +import { makeUserId } from '@/lib/user-account' export const runtime = 'nodejs' +const clean = (v: unknown): string | undefined => { + if (typeof v !== 'string') return undefined + const t = v.trim().replace(/^@/, '') + return t || undefined +} + function slugify(name: string): string { return name .toLowerCase() @@ -29,7 +38,15 @@ export async function POST(req: Request) { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) } - const { displayName, email, walletAddress, twitterHandle, bio, promoMethod } = body + const { displayName, email, walletAddress, twitterHandle, bio, promoMethod, telegram, linkedin, reddit } = body + + // Associate with the signed-in account when present (email session cookie). + let sessionEmail: string | undefined + try { + const raw = (await cookies()).get('malama_email_session')?.value + if (raw) sessionEmail = parseEmailSessionToken(raw)?.email?.toLowerCase() + } catch { /* no session — public application */ } + const resolvedEmail = sessionEmail ?? (email ? String(email).trim().toLowerCase() : undefined) if (!displayName || typeof displayName !== 'string' || !displayName.trim()) { return NextResponse.json({ error: 'displayName is required' }, { status: 400 }) @@ -58,9 +75,14 @@ export async function POST(req: Request) { id, walletAddress: String(walletAddress), displayName: String(displayName).trim(), - email: email ? String(email).trim().toLowerCase() : undefined, - twitterHandle: twitterHandle ? String(twitterHandle).replace(/^@/, '') : undefined, + email: resolvedEmail, + twitterHandle: clean(twitterHandle), + telegram: clean(telegram), + linkedin: clean(linkedin), + reddit: clean(reddit), bio: bio ? String(bio).trim() : undefined, + promoMethod: promoMethod ? String(promoMethod).trim() : undefined, + userId: resolvedEmail ? makeUserId(resolvedEmail) : undefined, commissionBps: 1000, // default 10% — admin can adjust approved: false, // requires admin approval }) diff --git a/apps/web/src/app/partners/apply/page.tsx b/apps/web/src/app/partners/apply/page.tsx index ef40b3c..0772db8 100644 --- a/apps/web/src/app/partners/apply/page.tsx +++ b/apps/web/src/app/partners/apply/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, FormEvent } from 'react' +import { useState, useEffect, FormEvent } from 'react' import { motion } from 'framer-motion' import { ArrowRight, CheckCircle2, AlertCircle, Twitter, Wallet, User, Mail, FileText } from 'lucide-react' import Link from 'next/link' @@ -22,13 +22,33 @@ export default function ApplyPage() { email: '', walletAddress: '', twitterHandle: '', + telegram: '', + linkedin: '', + reddit: '', bio: '', promoMethod: '', }) + const [prefilled, setPrefilled] = useState(false) const set = (k: keyof typeof form) => (e: React.ChangeEvent) => setForm((f) => ({ ...f, [k]: e.target.value })) + // Prefill from the signed-in account (email session + first owned EVM wallet). + useEffect(() => { + fetch('/api/user', { credentials: 'include' }) + .then((r) => (r.ok ? r.json() : null)) + .then((d: { account?: { email?: string; evmAddresses?: string[] } } | null) => { + if (!d?.account) return + setForm((f) => ({ + ...f, + email: f.email || d.account!.email || '', + walletAddress: f.walletAddress || d.account!.evmAddresses?.[0] || '', + })) + setPrefilled(true) + }) + .catch(() => {}) + }, []) + async function handleSubmit(e: FormEvent) { e.preventDefault() setState('submitting') @@ -118,6 +138,11 @@ export default function ApplyPage() {

+ {prefilled && ( +

+ ✓ Linked to your signed-in account — email & wallet prefilled. +

+ )} {/* Name */} @@ -180,8 +205,38 @@ export default function ApplyPage() {
- {/* Bio */} + {/* Other channels */} + +
+ + + +
+

+ Once approved, your dashboard generates ready-to-post copy + one-click share links for each. +

+
+ + {/* Bio */} + diff --git a/apps/web/src/app/partners/dashboard/page.tsx b/apps/web/src/app/partners/dashboard/page.tsx index 4490193..c0b044c 100644 --- a/apps/web/src/app/partners/dashboard/page.tsx +++ b/apps/web/src/app/partners/dashboard/page.tsx @@ -19,6 +19,7 @@ import { } from 'lucide-react' import Link from 'next/link' import type { KOLPartner, ReferralCommission } from '@/lib/kol-registry' +import { buildAmplifyPosts } from '@/lib/amplify' const fadeUp = { hidden: { opacity: 0, y: 14 }, @@ -241,10 +242,66 @@ function Dashboard({ data, onRefresh }: { data: DashboardData; onRefresh: () =>

- {/* Commission history */} + {/* Amplify — push your link */} +
+

Amplify — push your link

+ + Every sale = ${Math.round((2000 * data.partner.commissionBps) / 10000)} USDC + +
+

+ Ready-to-post copy with your link baked in. Post on X, Reddit DePIN subs, LinkedIn, Telegram and Discord — + the more you push, the more you earn. No caps, no waiting periods. +

+
+ {buildAmplifyPosts({ referralUrl: data.referralUrl, displayName: data.partner.displayName }).map((post) => ( +
+
+

{post.label}

+
+ + {post.shareUrl && ( + + Share → + + )} +
+
+

{post.text}

+ {post.subreddits && ( +
+ {post.subreddits.map((s) => ( + + r/{s} + + ))} +
+ )} +

{post.hint}

+
+ ))} +
+
+ + {/* Commission history */} +

Commission history diff --git a/apps/web/src/lib/amplify.ts b/apps/web/src/lib/amplify.ts new file mode 100644 index 0000000..8699fd6 --- /dev/null +++ b/apps/web/src/lib/amplify.ts @@ -0,0 +1,114 @@ +/** + * Amplify — partner "push" toolkit. + * + * Generates channel-specific, ready-to-post copy with the partner's referral + * link injected, plus one-click share-intent URLs where the platform supports + * prefill. Drives organic distribution across X, Reddit (DePIN subs), LinkedIn, + * Telegram, and Discord. + * + * Admin-editable templates can override these defaults later; this is the + * built-in baseline. + */ + +export type AmplifyChannel = 'x' | 'reddit' | 'linkedin' | 'telegram' | 'discord' + +export type AmplifyPost = { + channel: AmplifyChannel + label: string + /** Ready-to-post copy (referral link already embedded). */ + text: string + /** One-click composer URL prefilled where supported; null = copy & paste. */ + shareUrl: string | null + /** Reddit: link-post title (separate from URL). */ + title?: string + /** Reddit: suggested communities to post in. */ + subreddits?: string[] + /** Short how-to nudge shown under the action. */ + hint: string +} + +/** Key DePIN / crypto communities worth posting in. */ +export const DEPIN_SUBREDDITS = [ + 'depin', + 'CryptoCurrency', + 'helium', + 'Cardano', + 'IoTeX', +] as const + +const CAMPAIGN_HASHTAG = '#DePIN' + +export function buildAmplifyPosts(opts: { + referralUrl: string + displayName?: string + hashtag?: string +}): AmplifyPost[] { + const url = opts.referralUrl + const tag = opts.hashtag ?? CAMPAIGN_HASHTAG + const enc = encodeURIComponent + + // ── X / Twitter ────────────────────────────────────────────────────────── + const xText = + `Mālama Genesis is live — hardware-signed environmental + AI-compute data ` + + `anchored on @base and @Cardano. 200 Genesis Hex Nodes. Reserve yours 👉 ${url} ${tag} #Mālama` + + // ── Reddit (link post to a DePIN sub) ──────────────────────────────────── + const redditTitle = + `Mālama Genesis — hardware-signed DePIN for carbon + AI-compute data (200 Genesis Hex Nodes, on Base & Cardano)` + + // ── LinkedIn (professional framing) ────────────────────────────────────── + const linkedinText = + `Backing Mālama Labs — the trust anchor for physical-world data. Carbon dMRV (proven) ` + + `and AI-compute monitoring (scaling) on one hardware-signed architecture, anchored to Base ` + + `and Cardano. They're releasing 200 Genesis Hex Nodes. Reserve one: ${url}` + + // ── Telegram ───────────────────────────────────────────────────────────── + const telegramText = + `Mālama Genesis is live ⚡ 200 hardware-signed Hex Nodes for the physical-data network ` + + `(carbon dMRV + AI-compute), on Base + Cardano. Reserve yours 👉 ${url}` + + // ── Discord ────────────────────────────────────────────────────────────── + const discordText = + `**Mālama Genesis is live** — 200 hardware-signed Hex Nodes for the physical-data network ` + + `(carbon dMRV + AI-compute), anchored on Base + Cardano.\nReserve yours 👉 ${url}` + + return [ + { + channel: 'x', + label: 'X / Twitter', + text: xText, + shareUrl: `https://twitter.com/intent/tweet?text=${enc(xText)}`, + hint: 'Opens the tweet composer prefilled. Post it, then pin or retweet from the Mālama account.', + }, + { + channel: 'reddit', + label: 'Reddit (DePIN subs)', + text: redditTitle, + title: redditTitle, + subreddits: [...DEPIN_SUBREDDITS], + shareUrl: `https://www.reddit.com/r/${DEPIN_SUBREDDITS[0]}/submit?url=${enc(url)}&title=${enc(redditTitle)}`, + hint: 'Opens a link post in r/depin. Also share in r/CryptoCurrency, r/helium, r/Cardano, r/IoTeX. Read each sub’s self-promo rules first.', + }, + { + channel: 'linkedin', + label: 'LinkedIn', + text: linkedinText, + shareUrl: `https://www.linkedin.com/sharing/share-offsite/?url=${enc(url)}`, + hint: 'LinkedIn only prefills the link — copy the text above and paste it as your post commentary.', + }, + { + channel: 'telegram', + label: 'Telegram', + text: telegramText, + shareUrl: `https://t.me/share/url?url=${enc(url)}&text=${enc(telegramText)}`, + hint: 'Opens Telegram’s share sheet. Post in DePIN / crypto groups you’re part of.', + }, + { + channel: 'discord', + label: 'Discord', + text: discordText, + shareUrl: null, + hint: 'Copy and paste into DePIN / crypto Discord servers (Mālama, Base, Cardano communities).', + }, + ] +} diff --git a/apps/web/src/lib/kol-registry.ts b/apps/web/src/lib/kol-registry.ts index b65e569..dd51e90 100644 --- a/apps/web/src/lib/kol-registry.ts +++ b/apps/web/src/lib/kol-registry.ts @@ -26,6 +26,14 @@ export type KOLPartner = { displayName: string bio?: string twitterHandle?: string + /** Social handles for amplification / community pushes */ + telegram?: string + linkedin?: string + reddit?: string + /** How the partner plans to promote (captured at application) */ + promoMethod?: string + /** Linked user account id (sha256 of email) when applied while signed in */ + userId?: string /** Commission in basis points: 1000 = 10%, 1500 = 15%, 2000 = 20% */ commissionBps: number /** Must be true before referral links go live */ From f095e221c1d5c43d69853fdb6f1ef8f49316a11a Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 22:36:49 -0700 Subject: [PATCH 13/46] fix: partner dashboard pending-state crash, Lace late-connect, footer docs reorg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Partner dashboard: a 403 (application pending) was set as `data`, then data.clicks.toLocaleString() crashed the page. Handle 403/pending explicitly with an "Application under review" view; only render the dashboard with a valid stats payload (guards toLocaleString/toFixed on undefined). - Lace connect: enable() was raced against a 12s timeout and the late result discarded, so a successful (but slow) sign-in still showed "Lace is still starting up — click Connect again." Extend to 60s and recover a late enable() resolution (connect + clear the error automatically). - Footer Documentation: rename "Docs hub" → "Protocol Policy & Documentation" and move to top; reorder Whitepaper, Tokenomics, Pricing & Dynamics, Operator Guide, User Documentation. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/app/partners/dashboard/page.tsx | 31 ++++++++++++++++++-- apps/web/src/components/GenesisMint.tsx | 24 +++++++++++---- apps/web/src/components/SiteFooter.tsx | 8 ++--- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/apps/web/src/app/partners/dashboard/page.tsx b/apps/web/src/app/partners/dashboard/page.tsx index c0b044c..3087ed7 100644 --- a/apps/web/src/app/partners/dashboard/page.tsx +++ b/apps/web/src/app/partners/dashboard/page.tsx @@ -407,14 +407,26 @@ function Dashboard({ data, onRefresh }: { data: DashboardData; onRefresh: () => export default function PartnersDashboardPage() { const { address, isConnected } = useAccount() const [data, setData] = useState(null) - const [status, setStatus] = useState<'loading' | 'found' | 'not-found' | 'idle'>('idle') + const [pendingName, setPendingName] = useState(null) + const [status, setStatus] = useState<'loading' | 'found' | 'not-found' | 'pending' | 'idle'>('idle') const load = useCallback(async (addr: string) => { setStatus('loading') try { const res = await fetch(`/api/partners/me?address=${encodeURIComponent(addr)}`) if (res.status === 404) { setStatus('not-found'); return } - const json = await res.json() + const json = await res.json().catch(() => null) + // 403 = application exists but not yet approved → show pending, not the dashboard. + if (res.status === 403 || json?.status === 'pending') { + setPendingName(json?.partner?.displayName ?? null) + setStatus('pending') + return + } + // Only render the dashboard with a complete stats payload. + if (!res.ok || !json || typeof json.clicks !== 'number') { + setStatus('not-found') + return + } setData(json) setStatus('found') } catch { @@ -445,6 +457,21 @@ export default function PartnersDashboardPage() {

) } + if (status === 'pending') { + return ( +
+
+ +

Application under review

+

+ {pendingName ? `Thanks, ${pendingName}. ` : ''}Your partner application is pending approval. We review + within 24 hours — once approved, your stats, referral links, and the Amplify toolkit unlock here. +

+ Return home +
+
+ ) + } if (status === 'not-found' || !data) return return load(address!)} /> } diff --git a/apps/web/src/components/GenesisMint.tsx b/apps/web/src/components/GenesisMint.tsx index e979255..7b09d4b 100644 --- a/apps/web/src/components/GenesisMint.tsx +++ b/apps/web/src/components/GenesisMint.tsx @@ -175,14 +175,26 @@ export default function GenesisMint({ hexId }: { hexId: string | null }) { throw new Error(`not_found`) } - // Race enable() against a 12 s timeout. - // Lace's MV3 background service worker restarts periodically; the first - // enable() call sometimes hangs until it wakes back up. On timeout the - // user sees a friendly "try again" message — a second click usually works. + // enable() resolves when the user approves in Lace (rejects if declined). + // Lace's MV3 service worker can be slow to wake and first-time unlock + + // approve often takes >12s, so we use a generous timeout. Critically, if + // enable() resolves LATE (after the timeout fired), we still connect and + // clear the error — instead of discarding a successful sign-in and leaving + // the user stuck on "click Connect again". + const enablePromise = win.cardano[walletKey].enable() as Promise + enablePromise + .then((late) => { + setCardanoCip30Api(late) + setCardanoConnectError(null) + setCardanoConnecting(false) + connectCardano(walletKey).catch(() => {}) + }) + .catch(() => {}) + const api = await Promise.race([ - win.cardano[walletKey].enable() as Promise, + enablePromise, new Promise((_, reject) => - setTimeout(() => reject(new Error('timeout')), 12_000) + setTimeout(() => reject(new Error('timeout')), 60_000) ), ]) diff --git a/apps/web/src/components/SiteFooter.tsx b/apps/web/src/components/SiteFooter.tsx index ea14a46..0e09851 100644 --- a/apps/web/src/components/SiteFooter.tsx +++ b/apps/web/src/components/SiteFooter.tsx @@ -17,12 +17,12 @@ const footerSections: { title: string; links: { href: string; label: string; ext { title: 'Documentation', links: [ - { href: '/whitepaper', label: 'Whitepaper v1.0' }, - { href: '/docs', label: 'Docs hub' }, - { href: '/docs/tokenomics', label: 'MLMA Tokenomics' }, + { href: '/docs', label: 'Protocol Policy & Documentation' }, + { href: '/whitepaper', label: 'Whitepaper' }, + { href: '/docs/tokenomics', label: 'Tokenomics' }, { href: '/docs/pricing-roi', label: 'Pricing & Dynamics' }, { href: '/docs/operators', label: 'Operator Guide' }, - { href: 'https://docs.malamalabs.com', label: 'User Docs', external: true }, + { href: 'https://docs.malamalabs.com', label: 'User Documentation', external: true }, ], }, { From bc94ebdbba44db4714a179013ec7da76fcabf0c1 Mon Sep 17 00:00:00 2001 From: tyler malin Date: Sun, 7 Jun 2026 22:47:59 -0700 Subject: [PATCH 14/46] feat(admin): Amplify messaging management + partner activity overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Admins can edit the per-channel push copy + campaign hashtag from /admin/partners ("Amplify Messaging" editor). Stored in KV (lib/amplify-config.ts, server-only); blank fields fall back to built-in defaults. - buildAmplifyPosts now accepts AmplifyOverrides and derives share URLs from the final (possibly edited) text, so X/Telegram composers reflect admin copy. - /api/partners/me returns the overrides; the partner dashboard applies them — so admin edits flow to every partner's Amplify toolkit. - New /api/admin/amplify (x-admin-secret) + partners-proxy actions amplify-config (GET) and set-amplify (POST), behind email-session admin auth. - Admin activity overview: aggregate cards (partners, active, total clicks, conversions, total commissions) computed from the partner list. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/app/admin/partners/page.tsx | 108 ++++++++++++++++++ apps/web/src/app/api/admin/amplify/route.ts | 36 ++++++ .../src/app/api/admin/partners-proxy/route.ts | 16 +++ apps/web/src/app/api/partners/me/route.ts | 2 + apps/web/src/app/partners/dashboard/page.tsx | 5 +- apps/web/src/lib/amplify-config.ts | 37 ++++++ apps/web/src/lib/amplify.ts | 44 ++++--- 7 files changed, 232 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/app/api/admin/amplify/route.ts create mode 100644 apps/web/src/lib/amplify-config.ts diff --git a/apps/web/src/app/admin/partners/page.tsx b/apps/web/src/app/admin/partners/page.tsx index 8ad2b4c..c5c0b26 100644 --- a/apps/web/src/app/admin/partners/page.tsx +++ b/apps/web/src/app/admin/partners/page.tsx @@ -98,12 +98,52 @@ export default function AdminPartnersPage() { const [payoutBusy, setPayoutBusy] = useState(null); const [payoutMsg, setPayoutMsg] = useState(null); + const [amplify, setAmplify] = useState<{ hashtag?: string; posts?: Record }>({}); + const [amplifySaving, setAmplifySaving] = useState(false); + const [amplifyMsg, setAmplifyMsg] = useState(null); + const loadPayouts = () => fetch('/api/admin/partners-proxy?action=payouts') .then((r) => r.json()) .then((d) => { if (!d.error) setPayouts(d as PayoutsData); }) .catch(() => {}); + const loadAmplify = () => + fetch('/api/admin/partners-proxy?action=amplify-config') + .then((r) => r.json()) + .then((d) => { if (!d.error) setAmplify(d.config ?? {}); }) + .catch(() => {}); + + const AMPLIFY_CHANNELS: { key: string; label: string; placeholder: string }[] = [ + { key: 'x', label: 'X / Twitter', placeholder: 'Tweet copy — include [REFERRAL_URL] via the auto-injected link' }, + { key: 'reddit', label: 'Reddit (link-post title)', placeholder: 'Reddit post title' }, + { key: 'linkedin', label: 'LinkedIn', placeholder: 'LinkedIn post commentary' }, + { key: 'telegram', label: 'Telegram', placeholder: 'Telegram message' }, + { key: 'discord', label: 'Discord', placeholder: 'Discord message' }, + ]; + + const setAmplifyPost = (key: string, val: string) => + setAmplify((a) => ({ ...a, posts: { ...(a.posts ?? {}), [key]: val } })); + + const saveAmplify = async () => { + setAmplifySaving(true); + setAmplifyMsg(null); + try { + const res = await fetch('/api/admin/partners-proxy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'set-amplify', config: amplify }), + }); + const data = await res.json(); + if (!res.ok) setAmplifyMsg(`✕ ${data.error ?? 'Save failed'}`); + else { setAmplify(data.config ?? {}); setAmplifyMsg('Saved — every partner now sees this copy.'); } + } catch (e) { + setAmplifyMsg(`✕ ${String(e)}`); + } finally { + setAmplifySaving(false); + } + }; + // ── Load data ── useEffect(() => { Promise.all([ @@ -118,6 +158,7 @@ export default function AdminPartnersPage() { .catch((e) => setError(String(e))) .finally(() => setLoading(false)); loadPayouts(); + loadAmplify(); }, []); const copy = (text: string, id: string) => { @@ -181,6 +222,14 @@ export default function AdminPartnersPage() { const pending = partners.filter((p) => !p.approved); const active = partners.filter((p) => p.approved); + const totals = partners.reduce( + (acc, p) => ({ + clicks: acc.clicks + (p.stats?.clicks ?? 0), + conversions: acc.conversions + (p.stats?.conversions ?? 0), + earned: acc.earned + (p.stats?.totalEarned ?? 0), + }), + { clicks: 0, conversions: 0, earned: 0 }, + ); return ( @@ -199,6 +248,22 @@ export default function AdminPartnersPage() {
+ {/* ── Activity overview ── */} +
+ {[ + { label: 'Partners', value: String(partners.length) }, + { label: 'Active', value: String(active.length) }, + { label: 'Total clicks', value: totals.clicks.toLocaleString() }, + { label: 'Conversions', value: totals.conversions.toLocaleString() }, + { label: 'Commissions', value: `$${totals.earned.toFixed(0)}` }, + ].map((s) => ( +
+

{s.label}

+

{s.value}

+
+ ))} +
+ {/* ── Payouts ── */} {payouts && (
@@ -311,6 +376,49 @@ export default function AdminPartnersPage() {
{/* ── Approved copy templates ── */} + {/* ── Amplify messaging editor ── */} +
+

+ Edit the ready-to-post copy partners see in their dashboard's Amplify toolkit. Leave a field blank to use + the built-in default. Each partner's referral link is appended automatically. +

+
+ + setAmplify((a) => ({ ...a, hashtag: e.target.value }))} + placeholder="#DePIN" + style={{ width: '100%', maxWidth: 240, background: '#0d0d0d', border: '1px solid #222', borderRadius: 6, padding: '8px 10px', color: '#e8e8e8', fontSize: 13 }} + /> +
+ {AMPLIFY_CHANNELS.map((ch) => ( +
+ +