From be50e9b4b915c9aa8c99b423302695f53bf28ce2 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Tue, 21 Apr 2026 14:59:42 +0900 Subject: [PATCH 1/2] [#890] Add daily price snapshot cron for TWAP calculation Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/cron/airdrop-price/route.ts | 86 +++++++++++++++++++++++++ vercel.json | 4 ++ 2 files changed, 90 insertions(+) create mode 100644 src/app/api/cron/airdrop-price/route.ts diff --git a/src/app/api/cron/airdrop-price/route.ts b/src/app/api/cron/airdrop-price/route.ts new file mode 100644 index 00000000..1523eb38 --- /dev/null +++ b/src/app/api/cron/airdrop-price/route.ts @@ -0,0 +1,86 @@ +/** + * Daily price snapshot cron (#890) + * + * Records PLOT USD price, circulating supply, and mcap for TWAP calculation. + * Schedule: once/day at midnight UTC (0 0 * * *) + */ + +import { NextResponse } from "next/server"; +import { formatUnits } from "viem"; +import { createServerClient } from "../../../../../lib/supabase"; +import { getPlotUsdPrice } from "../../../../../lib/usd-price"; +import { publicClient } from "../../../../../lib/rpc"; +import { PLOT_TOKEN } from "../../../../../lib/contracts/constants"; +import { erc20Abi } from "../../../../../lib/price"; + +function verifyCron(req: Request): boolean { + const secret = process.env.CRON_SECRET; + if (!secret) { + return process.env.NODE_ENV !== "production"; + } + const authHeader = req.headers.get("authorization"); + return authHeader === `Bearer ${secret}`; +} + +export async function POST(req: Request) { + if (!verifyCron(req)) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const supabase = createServerClient(); + if (!supabase) { + return NextResponse.json({ error: "Supabase not configured" }, { status: 500 }); + } + + const todayUtc = new Date().toISOString().slice(0, 10); + + // Idempotency: skip if today already has an entry + const { data: existing } = await supabase + .from("pl_daily_prices") + .select("id") + .eq("recorded_at", todayUtc) + .limit(1) + .single(); + + if (existing) { + return NextResponse.json({ skipped: true, reason: "Entry already exists for today" }); + } + + // Fetch price — skip snapshot entirely if price fetch fails + const priceUsd = await getPlotUsdPrice(true); + if (priceUsd === null) { + console.error("[airdrop-price] Price fetch returned null — skipping snapshot for", todayUtc); + return NextResponse.json({ skipped: true, reason: "Price fetch failed" }, { status: 200 }); + } + + // Fetch circulating supply from on-chain + let supplyFormatted: number; + try { + const totalSupplyRaw = await publicClient.readContract({ + address: PLOT_TOKEN, + abi: erc20Abi, + functionName: "totalSupply", + }) as bigint; + supplyFormatted = Number(formatUnits(totalSupplyRaw, 18)); + } catch (err) { + console.error("[airdrop-price] Failed to fetch supply:", err); + return NextResponse.json({ skipped: true, reason: "Supply fetch failed" }, { status: 200 }); + } + + const mcapUsd = priceUsd * supplyFormatted; + + const { error } = await supabase.from("pl_daily_prices").insert({ + recorded_at: todayUtc, + price_usd: priceUsd, + supply: supplyFormatted, + mcap_usd: mcapUsd, + }); + + if (error) { + console.error("[airdrop-price] Insert failed:", error.message); + return NextResponse.json({ error: "Insert failed" }, { status: 500 }); + } + + console.info(`[airdrop-price] Snapshot recorded: date=${todayUtc} price=${priceUsd} supply=${supplyFormatted} mcap=${mcapUsd}`); + return NextResponse.json({ recorded: true, date: todayUtc, priceUsd, supply: supplyFormatted, mcapUsd }); +} diff --git a/vercel.json b/vercel.json index c9963fd9..71d52486 100644 --- a/vercel.json +++ b/vercel.json @@ -3,6 +3,10 @@ { "path": "/api/cron/backfill", "schedule": "*/5 * * * *" + }, + { + "path": "/api/cron/airdrop-price", + "schedule": "0 0 * * *" } ] } From 9244e966cf20629e0ff2210d222c3d80060aaf9f Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Tue, 21 Apr 2026 15:03:10 +0900 Subject: [PATCH 2/2] [#890] Fix cron handler to use GET for Vercel cron compatibility Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/cron/airdrop-price/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/cron/airdrop-price/route.ts b/src/app/api/cron/airdrop-price/route.ts index 1523eb38..c7b2b746 100644 --- a/src/app/api/cron/airdrop-price/route.ts +++ b/src/app/api/cron/airdrop-price/route.ts @@ -22,7 +22,7 @@ function verifyCron(req: Request): boolean { return authHeader === `Bearer ${secret}`; } -export async function POST(req: Request) { +export async function GET(req: Request) { if (!verifyCron(req)) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }