From 22a4bd56f5f952141178d601bc8f4e85d0c82b3b Mon Sep 17 00:00:00 2001 From: dev-agent Date: Tue, 26 May 2026 07:32:18 +0000 Subject: [PATCH 1/3] [#1247] Rewrite v1 endpoints: status, leaderboard, points deprecation status: v5 milestones ($100K/$1M/$5M/$10M), env_check.all_present, activation_count + eligible_activation_count, removed streak/weekly. leaderboard: rewritten to use weighted_spend RPC (T2.4b shared helper), ranked by weighted_spend DESC. points: deprecated to thin compat shim returning buy_volume_plot with Deprecation + Link successor-version headers. referral-code GET untouched (RE1 round-12). Closes #1247 Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 4 +- package.json | 2 +- src/app/api/airdrop/leaderboard/route.ts | 69 +++++++------- src/app/api/airdrop/points/route.ts | 113 ++++------------------- src/app/api/airdrop/status/route.ts | 106 ++++++++++++--------- 5 files changed, 117 insertions(+), 177 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e970f33..cc72ced8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plotlink", - "version": "1.33.1", + "version": "1.34.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "plotlink", - "version": "1.33.1", + "version": "1.34.0", "workspaces": [ "packages/*" ], diff --git a/package.json b/package.json index 7bb61bc4..4b9265f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "1.33.1", + "version": "1.34.0", "private": true, "workspaces": [ "packages/*" diff --git a/src/app/api/airdrop/leaderboard/route.ts b/src/app/api/airdrop/leaderboard/route.ts index 66d7412c..df68a64c 100644 --- a/src/app/api/airdrop/leaderboard/route.ts +++ b/src/app/api/airdrop/leaderboard/route.ts @@ -1,10 +1,6 @@ -/** - * Points leaderboard (#885) - * GET /api/airdrop/leaderboard?address=0x... (optional) - */ - import { NextResponse, type NextRequest } from "next/server"; import { createServerClient } from "../../../../../lib/supabase"; +import { getAirdropConfig } from "../../../../../lib/airdrop/config"; export async function GET(req: NextRequest) { const supabase = createServerClient(); @@ -16,37 +12,39 @@ export async function GET(req: NextRequest) { const page = Math.max(1, parseInt(req.nextUrl.searchParams.get("page") ?? "1", 10) || 1); const limit = Math.min(50, Math.max(1, parseInt(req.nextUrl.searchParams.get("limit") ?? "20", 10) || 20)); - // Aggregate points per address - const { data: allPoints } = await supabase - .from("pl_points") - .select("address, points"); + const config = getAirdropConfig(); - if (!allPoints || allPoints.length === 0) { - return NextResponse.json({ entries: [], userRank: null, totalParticipants: 0, page: 1, totalPages: 0, limit }); - } + const { data: rows, error } = await supabase.rpc("weighted_spend", { + p_campaign_start: config.CAMPAIGN_START.toISOString(), + p_campaign_end: config.CAMPAIGN_END.toISOString(), + p_min_referral_threshold: config.MIN_REFERRAL_THRESHOLD, + p_multiplier_per_ref: config.REFERRAL_MULTIPLIER_PER_REF, + p_multiplier_cap: config.REFERRAL_MULTIPLIER_CAP, + }); - // Sum points by address - const pointsByAddress = new Map(); - let globalTotal = 0; - for (const row of allPoints) { - const addr = row.address.toLowerCase(); - pointsByAddress.set(addr, (pointsByAddress.get(addr) ?? 0) + row.points); - globalTotal += row.points; + if (error) { + console.error("[leaderboard] weighted_spend RPC failed:", error.message); + return NextResponse.json({ error: "Failed to compute leaderboard" }, { status: 500 }); } - // Sort descending by points - const sorted = [...pointsByAddress.entries()] - .sort((a, b) => b[1] - a[1]); + const allRows = (rows ?? []) as Array<{ + address: string; + weighted_spend: number; + buy_volume: number; + qualified_refs: number; + has_fc_bonus: number; + multiplier: number; + community_total: number; + }>; - // Paginate - const totalParticipants = pointsByAddress.size; + const sorted = [...allRows].sort((a, b) => Number(b.weighted_spend) - Number(a.weighted_spend)); + + const totalParticipants = sorted.length; const totalPages = Math.ceil(totalParticipants / limit); const start = (page - 1) * limit; const pageSlice = sorted.slice(start, start + limit); - // Look up usernames for current page - const pageAddresses = pageSlice.map(([addr]) => addr); - + const pageAddresses = pageSlice.map(r => r.address); const { data: users } = await supabase .from("pl_referral_codes") .select("address, code, is_farcaster_username") @@ -56,18 +54,21 @@ export async function GET(req: NextRequest) { (users ?? []).map((u) => [u.address.toLowerCase(), u.is_farcaster_username ? u.code : null]), ); - const entries = pageSlice.map(([addr, pts], i) => ({ + const communityTotal = Number(sorted[0]?.community_total ?? 0); + + const entries = pageSlice.map((row, i) => ({ rank: start + i + 1, - address: addr, - username: usernameMap.get(addr) ?? null, - totalPoints: Math.round(pts * 100) / 100, - sharePercent: globalTotal > 0 ? Math.round((pts / globalTotal) * 10000) / 100 : 0, + address: row.address, + username: usernameMap.get(row.address) ?? null, + weighted_spend: Number(row.weighted_spend), + buy_volume: Number(row.buy_volume), + multiplier: Number(row.multiplier), + sharePercent: communityTotal > 0 ? Math.round((Number(row.weighted_spend) / communityTotal) * 10000) / 100 : 0, })); - // Find user's rank if requested let userRank: number | null = null; if (userAddress) { - const idx = sorted.findIndex(([addr]) => addr === userAddress); + const idx = sorted.findIndex(r => r.address === userAddress); userRank = idx >= 0 ? idx + 1 : null; } diff --git a/src/app/api/airdrop/points/route.ts b/src/app/api/airdrop/points/route.ts index 7772da90..7c724cb3 100644 --- a/src/app/api/airdrop/points/route.ts +++ b/src/app/api/airdrop/points/route.ts @@ -1,12 +1,6 @@ -/** - * User points breakdown (#885) - * GET /api/airdrop/points?address=0x... - */ - import { NextResponse, type NextRequest } from "next/server"; import { createServerClient } from "../../../../../lib/supabase"; -import { AIRDROP_CONFIG } from "../../../../../lib/airdrop/config"; -import { getStreakBoost, getNextTier } from "../../../../../lib/airdrop/streak"; +import { getAirdropConfig } from "../../../../../lib/airdrop/config"; export async function GET(req: NextRequest) { const supabase = createServerClient(); @@ -19,98 +13,29 @@ export async function GET(req: NextRequest) { return NextResponse.json({ error: "Missing address param" }, { status: 400 }); } - // Points breakdown by action + const config = getAirdropConfig(); const { data: points } = await supabase .from("pl_points") - .select("action, points") - .eq("address", address); - - const breakdown = { buy: 0, referral: 0, write: 0, rate: 0 }; - let totalPoints = 0; - for (const row of points ?? []) { - const action = row.action as keyof typeof breakdown; - if (action in breakdown) { - breakdown[action] += row.points; - } - totalPoints += row.points; - } - - // Total points across all users (for share %) - const { data: allPoints } = await supabase - .from("pl_points") - .select("points"); - const globalTotal = (allPoints ?? []).reduce((sum, r) => sum + r.points, 0); - const sharePercent = globalTotal > 0 ? (totalPoints / globalTotal) * 100 : 0; - - // Streak info - const { data: streak } = await supabase - .from("pl_streaks") - .select("current_streak, last_checkin, longest_streak") + .select("points") .eq("address", address) - .single(); - - const currentStreak = streak?.current_streak ?? 0; - const boostPercent = getStreakBoost(currentStreak) * 100; - const nextTier = getNextTier(currentStreak); + .eq("action", "buy") + .gte("created_at", config.CAMPAIGN_START.toISOString()) + .lte("created_at", config.CAMPAIGN_END.toISOString()); - const todayUtc = new Date().toISOString().slice(0, 10); - const checkedInToday = streak?.last_checkin - ? new Date(streak.last_checkin).toISOString().slice(0, 10) === todayUtc - : false; + const buyVolume = (points ?? []).reduce((sum, r) => sum + r.points, 0); - // Referral info - const { data: referralCode } = await supabase - .from("pl_referral_codes") - .select("code, is_farcaster_username") - .eq("address", address) - .single(); - - const { data: referredBy } = await supabase - .from("pl_referrals") - .select("referral_code") - .eq("referred_address", address) - .single(); - - const { count: referredUsersCount } = await supabase - .from("pl_referrals") - .select("id", { count: "exact", head: true }) - .eq("referrer_address", address); - - // Estimated airdrop per milestone tier - const estimatedAirdrop = sharePercent > 0 - ? { - bronze: Math.round((sharePercent / 100) * AIRDROP_CONFIG.POOL_AMOUNT * (AIRDROP_CONFIG.MILESTONES.BRONZE.pct / 100)), - silver: Math.round((sharePercent / 100) * AIRDROP_CONFIG.POOL_AMOUNT * (AIRDROP_CONFIG.MILESTONES.SILVER.pct / 100)), - gold: Math.round((sharePercent / 100) * AIRDROP_CONFIG.POOL_AMOUNT * (AIRDROP_CONFIG.MILESTONES.GOLD.pct / 100)), - diamond: Math.round((sharePercent / 100) * AIRDROP_CONFIG.POOL_AMOUNT * (AIRDROP_CONFIG.MILESTONES.DIAMOND.pct / 100)), - } - : { bronze: 0, silver: 0, gold: 0, diamond: 0 }; - - return NextResponse.json({ - address, - totalPoints: Math.round(totalPoints * 100) / 100, - sharePercent: Math.round(sharePercent * 100) / 100, - breakdown: { - buy: Math.round(breakdown.buy * 100) / 100, - referral: Math.round(breakdown.referral * 100) / 100, - write: Math.round(breakdown.write * 100) / 100, - rate: Math.round(breakdown.rate * 100) / 100, - }, - streak: { - currentStreak, - boostPercent, - nextTier, - checkedInToday, - lastCheckin: streak?.last_checkin ?? null, + return NextResponse.json( + { + address, + buy_volume_plot: buyVolume, + fetched_at: new Date().toISOString(), }, - referral: { - code: referralCode?.code ?? null, - isFarcasterUsername: referralCode?.is_farcaster_username ?? false, - referredBy: referredBy?.referral_code ?? null, - referredUsersCount: referredUsersCount ?? 0, + { + headers: { + "Cache-Control": "public, s-maxage=10, stale-while-revalidate=5", + "Deprecation": "true", + "Link": "; rel=\"successor-version\"", + }, }, - estimatedAirdrop, - }, { - headers: { "Cache-Control": "public, s-maxage=10, stale-while-revalidate=5" }, - }); + ); } diff --git a/src/app/api/airdrop/status/route.ts b/src/app/api/airdrop/status/route.ts index 85535e3f..8560fbfd 100644 --- a/src/app/api/airdrop/status/route.ts +++ b/src/app/api/airdrop/status/route.ts @@ -1,77 +1,90 @@ -/** - * Campaign status overview (#885) - * GET /api/airdrop/status — no auth required - */ - import { NextResponse } from "next/server"; import { createServerClient } from "../../../../../lib/supabase"; -import { AIRDROP_CONFIG } from "../../../../../lib/airdrop/config"; +import { getAirdropConfig } from "../../../../../lib/airdrop/config"; import { getPlotUsdPrice } from "../../../../../lib/usd-price"; +function checkEnvConfig(): boolean { + const secrets = [ + process.env.TWITTERAPI_IO_KEY, + process.env.NEYNAR_API_KEY, + process.env.SUPABASE_SERVICE_ROLE_KEY, + process.env.CRON_SECRET, + ]; + if (secrets.some(s => !s)) return false; + + const config = getAirdropConfig(); + if (!config.SIWE_DOMAIN || !config.SIWE_URI || !config.SIWE_STATEMENT) return false; + if (!config.SIWE_CHAIN_ID || !config.PLOTLINK_X_HANDLE) return false; + if (!config.PLOTLINK_FC_FID) return false; + if (config.POOL_AMOUNT <= 0) return false; + if (config.CAMPAIGN_START >= config.CAMPAIGN_END) return false; + if (!config.MILESTONES.BRONZE || !config.MILESTONES.SILVER || !config.MILESTONES.GOLD || !config.MILESTONES.DIAMOND) return false; + + return true; +} + export async function GET() { const supabase = createServerClient(); if (!supabase) { return NextResponse.json({ error: "Supabase not configured" }, { status: 500 }); } + const config = getAirdropConfig(); const now = new Date(); - const start = AIRDROP_CONFIG.CAMPAIGN_START; - const end = AIRDROP_CONFIG.CAMPAIGN_END; + const start = config.CAMPAIGN_START; + const end = config.CAMPAIGN_END; const totalMs = end.getTime() - start.getTime(); const elapsedMs = Math.max(0, now.getTime() - start.getTime()); const remainingMs = Math.max(0, end.getTime() - now.getTime()); - // Latest price: try pl_daily_prices first, fall back to live price - const { data: latestPrice } = await supabase - .from("pl_daily_prices") - .select("price_usd, mcap_usd") - .order("recorded_at", { ascending: false }) - .limit(1) - .single(); + const [priceRes, activationRes, eligibleRes] = await Promise.all([ + supabase + .from("pl_daily_prices") + .select("price_usd, mcap_usd") + .order("recorded_at", { ascending: false }) + .limit(1) + .single(), + supabase + .from("pl_activations") + .select("address", { count: "exact", head: true }) + .not("activated_at", "is", null), + supabase + .from("pl_activations") + .select("address", { count: "exact", head: true }) + .not("activated_at", "is", null) + .eq("is_blacklisted", false), + ]); - // Live price fallback when daily snapshots haven't been recorded yet + const latestPrice = priceRes.data; const livePriceUsd = latestPrice?.price_usd ?? (await getPlotUsdPrice()); - // Total points earned + unique participants - const { data: allPoints } = await supabase - .from("pl_points") - .select("address, points"); - - let totalPointsEarned = 0; - const uniqueAddresses = new Set(); - for (const row of allPoints ?? []) { - totalPointsEarned += row.points; - uniqueAddresses.add(row.address); - } - const totalParticipants = uniqueAddresses.size; - - // Milestone status — use stored FDV or compute from live price const MAX_SUPPLY = 1_000_000; const currentFdv = latestPrice?.mcap_usd ? Number(latestPrice.mcap_usd) : livePriceUsd ? livePriceUsd * MAX_SUPPLY : 0; + const milestones = { bronze: { - mcap: AIRDROP_CONFIG.MILESTONES.BRONZE.mcap, - pct: AIRDROP_CONFIG.MILESTONES.BRONZE.pct, - reached: currentFdv >= AIRDROP_CONFIG.MILESTONES.BRONZE.mcap, + mcap: config.MILESTONES.BRONZE.mcap, + pct: config.MILESTONES.BRONZE.pct, + reached: currentFdv >= config.MILESTONES.BRONZE.mcap, }, silver: { - mcap: AIRDROP_CONFIG.MILESTONES.SILVER.mcap, - pct: AIRDROP_CONFIG.MILESTONES.SILVER.pct, - reached: currentFdv >= AIRDROP_CONFIG.MILESTONES.SILVER.mcap, + mcap: config.MILESTONES.SILVER.mcap, + pct: config.MILESTONES.SILVER.pct, + reached: currentFdv >= config.MILESTONES.SILVER.mcap, }, gold: { - mcap: AIRDROP_CONFIG.MILESTONES.GOLD.mcap, - pct: AIRDROP_CONFIG.MILESTONES.GOLD.pct, - reached: currentFdv >= AIRDROP_CONFIG.MILESTONES.GOLD.mcap, + mcap: config.MILESTONES.GOLD.mcap, + pct: config.MILESTONES.GOLD.pct, + reached: currentFdv >= config.MILESTONES.GOLD.mcap, }, diamond: { - mcap: AIRDROP_CONFIG.MILESTONES.DIAMOND.mcap, - pct: AIRDROP_CONFIG.MILESTONES.DIAMOND.pct, - reached: currentFdv >= AIRDROP_CONFIG.MILESTONES.DIAMOND.mcap, + mcap: config.MILESTONES.DIAMOND.mcap, + pct: config.MILESTONES.DIAMOND.pct, + reached: currentFdv >= config.MILESTONES.DIAMOND.mcap, }, }; @@ -80,13 +93,14 @@ export async function GET() { campaignEnd: end.toISOString().slice(0, 10), timeRemainingDays: Math.ceil(remainingMs / (1000 * 60 * 60 * 24)), timeElapsedPercent: totalMs > 0 ? Math.min(100, Math.round((elapsedMs / totalMs) * 100)) : 0, - poolAmount: AIRDROP_CONFIG.POOL_AMOUNT, + poolAmount: config.POOL_AMOUNT, currentFdv, latestPriceUsd: livePriceUsd ?? null, milestones, - totalPointsEarned, - totalParticipants, - lockerTx: AIRDROP_CONFIG.LOCKER_TX, + activation_count: activationRes.count ?? 0, + eligible_activation_count: eligibleRes.count ?? 0, + env_check: { all_present: checkEnvConfig() }, + lockerTx: config.LOCKER_TX, }, { headers: { "Cache-Control": "public, s-maxage=60, stale-while-revalidate=30" }, }); From 4e1bb048ad324fc2c37656f8769553b16e739c6c Mon Sep 17 00:00:00 2001 From: dev-agent Date: Tue, 26 May 2026 07:35:01 +0000 Subject: [PATCH 2/3] [#1247] Preserve backward-compat response shapes for callers leaderboard: add totalPoints alias (= weighted_spend) for Leaderboard.tsx compat. points: restore full v1 response shape (totalPoints, sharePercent, breakdown, streak stub, referral stub, estimatedAirdrop) for UserPoints.tsx compat, plus deprecation headers and buy_volume_plot. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/airdrop/leaderboard/route.ts | 1 + src/app/api/airdrop/points/route.ts | 80 ++++++++++++++++++------ 2 files changed, 61 insertions(+), 20 deletions(-) diff --git a/src/app/api/airdrop/leaderboard/route.ts b/src/app/api/airdrop/leaderboard/route.ts index df68a64c..6359ad45 100644 --- a/src/app/api/airdrop/leaderboard/route.ts +++ b/src/app/api/airdrop/leaderboard/route.ts @@ -61,6 +61,7 @@ export async function GET(req: NextRequest) { address: row.address, username: usernameMap.get(row.address) ?? null, weighted_spend: Number(row.weighted_spend), + totalPoints: Number(row.weighted_spend), buy_volume: Number(row.buy_volume), multiplier: Number(row.multiplier), sharePercent: communityTotal > 0 ? Math.round((Number(row.weighted_spend) / communityTotal) * 10000) / 100 : 0, diff --git a/src/app/api/airdrop/points/route.ts b/src/app/api/airdrop/points/route.ts index 7c724cb3..95fa3aca 100644 --- a/src/app/api/airdrop/points/route.ts +++ b/src/app/api/airdrop/points/route.ts @@ -14,28 +14,68 @@ export async function GET(req: NextRequest) { } const config = getAirdropConfig(); + const { data: points } = await supabase .from("pl_points") - .select("points") - .eq("address", address) - .eq("action", "buy") - .gte("created_at", config.CAMPAIGN_START.toISOString()) - .lte("created_at", config.CAMPAIGN_END.toISOString()); - - const buyVolume = (points ?? []).reduce((sum, r) => sum + r.points, 0); - - return NextResponse.json( - { - address, - buy_volume_plot: buyVolume, - fetched_at: new Date().toISOString(), + .select("action, points") + .eq("address", address); + + const breakdown = { buy: 0, referral: 0, write: 0, rate: 0 }; + let totalPoints = 0; + for (const row of points ?? []) { + const action = row.action as keyof typeof breakdown; + if (action in breakdown) { + breakdown[action] += row.points; + } + totalPoints += row.points; + } + + const { data: allPoints } = await supabase + .from("pl_points") + .select("points"); + const globalTotal = (allPoints ?? []).reduce((sum, r) => sum + r.points, 0); + const sharePercent = globalTotal > 0 ? (totalPoints / globalTotal) * 100 : 0; + + const estimatedAirdrop = sharePercent > 0 + ? { + bronze: Math.round((sharePercent / 100) * config.POOL_AMOUNT * (config.MILESTONES.BRONZE.pct / 100)), + silver: Math.round((sharePercent / 100) * config.POOL_AMOUNT * (config.MILESTONES.SILVER.pct / 100)), + gold: Math.round((sharePercent / 100) * config.POOL_AMOUNT * (config.MILESTONES.GOLD.pct / 100)), + diamond: Math.round((sharePercent / 100) * config.POOL_AMOUNT * (config.MILESTONES.DIAMOND.pct / 100)), + } + : { bronze: 0, silver: 0, gold: 0, diamond: 0 }; + + return NextResponse.json({ + address, + totalPoints: Math.round(totalPoints * 100) / 100, + sharePercent: Math.round(sharePercent * 100) / 100, + breakdown: { + buy: Math.round(breakdown.buy * 100) / 100, + referral: Math.round(breakdown.referral * 100) / 100, + write: Math.round(breakdown.write * 100) / 100, + rate: Math.round(breakdown.rate * 100) / 100, + }, + streak: { + currentStreak: 0, + boostPercent: 0, + nextTier: null, + checkedInToday: false, + lastCheckin: null, + }, + referral: { + code: null, + isFarcasterUsername: false, + referredBy: null, + referredUsersCount: 0, }, - { - headers: { - "Cache-Control": "public, s-maxage=10, stale-while-revalidate=5", - "Deprecation": "true", - "Link": "; rel=\"successor-version\"", - }, + estimatedAirdrop, + buy_volume_plot: breakdown.buy, + fetched_at: new Date().toISOString(), + }, { + headers: { + "Cache-Control": "public, s-maxage=10, stale-while-revalidate=5", + "Deprecation": "true", + "Link": "; rel=\"successor-version\"", }, - ); + }); } From a93fc5ba40af446c7779d3eb50eb38610891cbe0 Mon Sep 17 00:00:00 2001 From: dev-agent Date: Tue, 26 May 2026 07:37:21 +0000 Subject: [PATCH 3/3] [#1247] Restore live referral + streak queries in points shim Referral code, referredBy, referredUsersCount now queried from DB so UserPoints share link and "who referred you" input work correctly. Streak data queried but boostPercent hardcoded to 0 (v5 empty boosts). All queries parallelized via Promise.all. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/airdrop/points/route.ts | 39 +++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/app/api/airdrop/points/route.ts b/src/app/api/airdrop/points/route.ts index 95fa3aca..62bc0217 100644 --- a/src/app/api/airdrop/points/route.ts +++ b/src/app/api/airdrop/points/route.ts @@ -15,14 +15,18 @@ export async function GET(req: NextRequest) { const config = getAirdropConfig(); - const { data: points } = await supabase - .from("pl_points") - .select("action, points") - .eq("address", address); + const [pointsRes, allPointsRes, streakRes, referralCodeRes, referredByRes, referredCountRes] = await Promise.all([ + supabase.from("pl_points").select("action, points").eq("address", address), + supabase.from("pl_points").select("points"), + supabase.from("pl_streaks").select("current_streak, last_checkin, longest_streak").eq("address", address).single(), + supabase.from("pl_referral_codes").select("code, is_farcaster_username").eq("address", address).single(), + supabase.from("pl_referrals").select("referral_code").eq("referred_address", address).single(), + supabase.from("pl_referrals").select("id", { count: "exact", head: true }).eq("referrer_address", address), + ]); const breakdown = { buy: 0, referral: 0, write: 0, rate: 0 }; let totalPoints = 0; - for (const row of points ?? []) { + for (const row of pointsRes.data ?? []) { const action = row.action as keyof typeof breakdown; if (action in breakdown) { breakdown[action] += row.points; @@ -30,12 +34,15 @@ export async function GET(req: NextRequest) { totalPoints += row.points; } - const { data: allPoints } = await supabase - .from("pl_points") - .select("points"); - const globalTotal = (allPoints ?? []).reduce((sum, r) => sum + r.points, 0); + const globalTotal = (allPointsRes.data ?? []).reduce((sum, r) => sum + r.points, 0); const sharePercent = globalTotal > 0 ? (totalPoints / globalTotal) * 100 : 0; + const currentStreak = streakRes.data?.current_streak ?? 0; + const todayUtc = new Date().toISOString().slice(0, 10); + const checkedInToday = streakRes.data?.last_checkin + ? new Date(streakRes.data.last_checkin).toISOString().slice(0, 10) === todayUtc + : false; + const estimatedAirdrop = sharePercent > 0 ? { bronze: Math.round((sharePercent / 100) * config.POOL_AMOUNT * (config.MILESTONES.BRONZE.pct / 100)), @@ -56,17 +63,17 @@ export async function GET(req: NextRequest) { rate: Math.round(breakdown.rate * 100) / 100, }, streak: { - currentStreak: 0, + currentStreak, boostPercent: 0, nextTier: null, - checkedInToday: false, - lastCheckin: null, + checkedInToday, + lastCheckin: streakRes.data?.last_checkin ?? null, }, referral: { - code: null, - isFarcasterUsername: false, - referredBy: null, - referredUsersCount: 0, + code: referralCodeRes.data?.code ?? null, + isFarcasterUsername: referralCodeRes.data?.is_farcaster_username ?? false, + referredBy: referredByRes.data?.referral_code ?? null, + referredUsersCount: referredCountRes.count ?? 0, }, estimatedAirdrop, buy_volume_plot: breakdown.buy,