import { NextResponse } from "next/server"; const MEMPOOL = "https://mempool.space/api"; const MIN_CONFIRMATIONS = 1; function merchantAddress(): string { return ( process.env.MERCHANT_BTC_ADDRESS || process.env.NEXT_PUBLIC_MERCHANT_BTC_ADDRESS || "" ).trim(); } export async function POST(req: Request) { const merchant = merchantAddress(); if (!merchant) { return NextResponse.json( { ok: false, error: "Server not configured: set MERCHANT_BTC_ADDRESS" }, { status: 503 }, ); } let body: unknown; try { body = await req.json(); } catch { return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 }); } const txid = typeof body === "object" && body !== null && "txid" in body && typeof (body as { txid: unknown }).txid === "string" ? (body as { txid: string }).txid.trim() : ""; if (!/^[a-fA-F0-9]{64}$/.test(txid)) { return NextResponse.json({ ok: false, error: "Invalid transaction id" }, { status: 400 }); } const txRes = await fetch(`${MEMPOOL}/tx/${txid}`, { cache: "no-store" }); if (!txRes.ok) { return NextResponse.json( { ok: false, error: "Transaction not found on network yet" }, { status: 404 }, ); } const tx = (await txRes.json()) as { vout?: Array<{ value?: number; scriptpubkey_address?: string }>; status?: { confirmed?: boolean; block_height?: number }; }; const merchantLower = merchant.toLowerCase(); let sats = 0; for (const vout of tx.vout || []) { const addr = vout.scriptpubkey_address; if (addr && addr.toLowerCase() === merchantLower && typeof vout.value === "number") { sats += vout.value; } } if (sats <= 0) { return NextResponse.json( { ok: false, error: "This transaction does not pay your configured merchant Bitcoin address", }, { status: 400 }, ); } let confirmations = 0; if (tx.status?.confirmed && typeof tx.status.block_height === "number") { const tipRes = await fetch(`${MEMPOOL}/blocks/tip/height`, { cache: "no-store" }); if (tipRes.ok) { const tip = Number(await tipRes.text()); if (Number.isFinite(tip)) { confirmations = tip - tx.status.block_height + 1; } } } if (confirmations < MIN_CONFIRMATIONS) { return NextResponse.json({ ok: false, error: `Need at least ${MIN_CONFIRMATIONS} confirmation(s); currently ${confirmations}`, confirmations, sats, }); } const priceRes = await fetch( "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", { next: { revalidate: 60 } }, ); if (!priceRes.ok) { return NextResponse.json( { ok: false, error: "Could not fetch BTC/USD rate; try again shortly" }, { status: 502 }, ); } const priceJson = (await priceRes.json()) as { bitcoin?: { usd?: number } }; const btcUsd = priceJson.bitcoin?.usd; if (!btcUsd || !Number.isFinite(btcUsd)) { return NextResponse.json({ ok: false, error: "Invalid rate response" }, { status: 502 }); } const btc = sats / 1e8; const creditedUsd = Math.round(btc * btcUsd * 100) / 100; return NextResponse.json({ ok: true, creditedUsd, sats, btc, btcUsd, confirmations, }); }