← back to kingkpink__metalstats

Function bodies 642 total

All specs Real LLM only Function bodies
POST function · typescript · L6-L26 (21 LOC)
app/api/init-db/route.ts
export async function POST(request: Request) {
  if (!isAuthorized(request)) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  try {
    await initializeDatabase();
    await initializeForecastTables();
    await initializePriceCacheTable();
    return NextResponse.json({ 
      success: true, 
      message: 'Database initialized successfully (including forecast + price cache tables)' 
    });
  } catch (error) {
    console.error('Database initialization error:', error);
    return NextResponse.json(
      { success: false, error: 'Failed to initialize database' },
      { status: 500 }
    );
  }
}
GET function · typescript · L29-L49 (21 LOC)
app/api/init-db/route.ts
export async function GET(request: Request) {
  if (!isAuthorized(request)) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  try {
    await initializeDatabase();
    await initializeForecastTables();
    await initializePriceCacheTable();
    return NextResponse.json({ 
      success: true, 
      message: 'Database is ready' 
    });
  } catch (error) {
    console.error('Database check error:', error);
    return NextResponse.json(
      { success: false, error: 'Database not ready' },
      { status: 500 }
    );
  }
}
fetchYTDData function · typescript · L30-L35 (6 LOC)
app/api/market-comparison/route.ts
async function fetchYTDData(symbol: string): Promise<{
  price: number;
  ytdStartPrice: number;
  prevClose: number;
  marketState: string;
} | null> {
GET function · typescript · L106-L172 (67 LOC)
app/api/market-comparison/route.ts
export async function GET() {
  try {
    const results = await Promise.all(
      TICKERS.map(async (t) => {
        const data = await fetchYTDData(t.symbol);
        if (!data) return null;

        const ytdChangePercent = ((data.price - data.ytdStartPrice) / data.ytdStartPrice) * 100;
        const dailyChange = data.price - data.prevClose;
        const dailyChangePercent = data.prevClose > 0 ? (dailyChange / data.prevClose) * 100 : 0;

        return {
          ...t,
          price: Math.round(data.price * 100) / 100,
          ytdStartPrice: Math.round(data.ytdStartPrice * 100) / 100,
          ytdChangePercent: Math.round(ytdChangePercent * 100) / 100,
          dailyChange: Math.round(dailyChange * 100) / 100,
          dailyChangePercent: Math.round(dailyChangePercent * 100) / 100,
          marketState: data.marketState,
        } as TickerData & { group: string };
      })
    );

    const valid = results.filter(Boolean) as (TickerData & { group: string })[];

    const
GET function · typescript · L8-L54 (47 LOC)
app/api/metals/[metal]/history/route.ts
export async function GET(
  request: Request,
  { params }: { params: Promise<{ metal: string }> }
) {
  try {
    const { metal } = await params;
    
    // Parse query params for days
    const url = new URL(request.url);
    const daysParam = url.searchParams.get('days');
    const days = daysParam ? parseInt(daysParam, 10) : 90;

    if (isNaN(days) || days < 1 || days > 365) {
      return NextResponse.json(
        { success: false, error: 'Invalid days parameter (1-365)' },
        { status: 400 }
      );
    }

    const history = await getMetalHistory(metal, days);

    if (history.length === 0) {
      return NextResponse.json(
        { success: false, error: `No history found for ${metal}` },
        { status: 404 }
      );
    }

    return NextResponse.json({
      success: true,
      metal,
      days,
      data: history.map(h => ({
        date: String(h.report_date).slice(0, 10),
        registered: Number(h.registered),
        eligible: Number(h.eligible),
    
GET function · typescript · L8-L37 (30 LOC)
app/api/metals/route.ts
export async function GET() {
  try {
    const data = await getWarehouseDataWithChanges();
    
    // Check if we have data
    const snapshots = await getLatestSnapshots();
    if (snapshots.length === 0) {
      return NextResponse.json(
        { 
          success: false, 
          error: 'No data available. Please run the data sync first.',
          data: null 
        },
        { status: 404 }
      );
    }

    return NextResponse.json({ 
      success: true, 
      data,
      lastUpdated: new Date().toISOString()
    });
  } catch (error) {
    console.error('Error fetching metals data:', error);
    return NextResponse.json(
      { success: false, error: 'Failed to fetch metals data' },
      { status: 500 }
    );
  }
}
POST function · typescript · L26-L104 (79 LOC)
app/api/metals/sync/route.ts
export async function POST(request: Request) {
  if (!isAuthorized(request)) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  try {
    // Ensure database tables exist
    await initializeDatabase();

    const body = await request.json();
    
    // Validate the data structure
    if (!body || typeof body !== 'object') {
      return NextResponse.json(
        { success: false, error: 'Invalid data format' },
        { status: 400 }
      );
    }

    // Whitelist of valid metal keys
    const VALID_METALS = ['Gold', 'Silver', 'Copper', 'Aluminum', 'Zinc', 'Lead', 'Platinum_Palladium'];

    const results: { metal: string; success: boolean; error?: string }[] = [];

    // Process each metal in the payload
    for (const [metalKey, metalData] of Object.entries(body)) {
      if (!VALID_METALS.includes(metalKey)) {
        results.push({ metal: metalKey, success: false, error: 'Invalid metal' });
        continue;
      }
      try {
        const da
Powered by Repobility — scan your code at https://repobility.com
getResend function · typescript · L13-L15 (3 LOC)
app/api/newsletter/send/route.ts
function getResend() {
  return new Resend(process.env.RESEND_API_KEY);
}
POST function · typescript · L17-L115 (99 LOC)
app/api/newsletter/send/route.ts
export async function POST(request: Request) {
  if (!isAuthorized(request)) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  if (!isDatabaseAvailable()) {
    return NextResponse.json({ error: 'Database unavailable' }, { status: 503 });
  }

  try {
    // ─── Step 1: Generate the newsletter via the analysis engine ───
    const newsletter = await generateNewsletter();

    if (!newsletter) {
      return NextResponse.json({ error: 'Analysis data not available — cannot generate newsletter' }, { status: 500 });
    }

    // ─── Step 2: Store the generated newsletter in the DB ───
    try {
      await saveNewsletter(
        newsletter.reportDate,
        newsletter.subject,
        newsletter.html,
        newsletter.metalsAnalyzed,
        newsletter.avgRiskScore,
      );
      console.log(`Newsletter for ${newsletter.reportDate} saved to DB`);
    } catch (err) {
      console.error('Failed to save newsletter to DB (continuing with send):', err);
buildTrialExpiredEmail function · typescript · L119-L200 (82 LOC)
app/api/newsletter/send/route.ts
function buildTrialExpiredEmail(_email: string, unsubscribeUrl: string): string {
  const checkoutUrl = process.env.STRIPE_CHECKOUT_URL || 'https://buy.stripe.com/fZucN6fAj1e9fwugMpfw402';

  return `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin:0;padding:0;background-color:#0f172a;font-family:'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
  <table width="100%" cellpadding="0" cellspacing="0" style="background-color:#0f172a;padding:40px 20px;">
    <tr>
      <td align="center">
        <table width="600" cellpadding="0" cellspacing="0" style="background-color:#1e293b;border-radius:12px;overflow:hidden;">
          <!-- Header -->
          <tr>
            <td style="background:linear-gradient(135deg,#f59e0b,#d97706);padding:32px 40px;text-align:center;">
              <h1 style="margin:0;font-size:24px;color:#0f172a;font-weight:800;letter-spacing:-0.5px;">
       
getResend function · typescript · L7-L9 (3 LOC)
app/api/newsletter/subscribe/route.ts
function getResend() {
  return new Resend(process.env.RESEND_API_KEY);
}
isValidEmail function · typescript · L12-L15 (4 LOC)
app/api/newsletter/subscribe/route.ts
function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}
POST function · typescript · L17-L81 (65 LOC)
app/api/newsletter/subscribe/route.ts
export async function POST(request: Request) {
  // Rate limit: 3 subscribe attempts per IP per 15 minutes
  const ip = getClientIp(request);
  const { limited } = rateLimit(`subscribe:${ip}`, 3, 15 * 60 * 1000);
  if (limited) {
    return NextResponse.json(
      { success: false, error: 'Too many attempts. Please try again later.' },
      { status: 429 },
    );
  }

  try {
    const body = await request.json();
    const { email } = body;

    if (!email || !isValidEmail(email)) {
      return NextResponse.json(
        { success: false, error: 'Please enter a valid email address.' },
        { status: 400 }
      );
    }

    if (!isDatabaseAvailable()) {
      return NextResponse.json(
        { success: false, error: 'Service temporarily unavailable. Please try again later.' },
        { status: 503 }
      );
    }

    // Check if already subscribed
    const alreadySubscribed = await isSubscribed(email);
    if (alreadySubscribed) {
      return NextResponse.json(
        
getWelcomeEmailHtml function · typescript · L83-L166 (84 LOC)
app/api/newsletter/subscribe/route.ts
function getWelcomeEmailHtml(unsubscribeUrl: string): string {
  return `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin:0;padding:0;background-color:#0f172a;font-family:'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
  <table width="100%" cellpadding="0" cellspacing="0" style="background-color:#0f172a;padding:40px 20px;">
    <tr>
      <td align="center">
        <table width="600" cellpadding="0" cellspacing="0" style="background-color:#1e293b;border-radius:12px;overflow:hidden;">
          <!-- Header -->
          <tr>
            <td style="background:linear-gradient(135deg,#f59e0b,#d97706);padding:32px 40px;text-align:center;">
              <h1 style="margin:0;font-size:24px;color:#0f172a;font-weight:800;letter-spacing:-0.5px;">
                HEAVY METAL STATS
              </h1>
              <p style="margin:8px 0 0;font-size:14px;color:#451a03;font-weight:
GET function · typescript · L4-L43 (40 LOC)
app/api/newsletter/unsubscribe/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const token = searchParams.get('token');

  if (!token) {
    return new NextResponse(getUnsubscribeHtml(false, 'Invalid unsubscribe link.'), {
      status: 400,
      headers: { 'Content-Type': 'text/html' },
    });
  }

  if (!isDatabaseAvailable()) {
    return new NextResponse(getUnsubscribeHtml(false, 'Service temporarily unavailable. Please try again later.'), {
      status: 503,
      headers: { 'Content-Type': 'text/html' },
    });
  }

  try {
    const removed = await removeSubscriber(token);

    if (removed) {
      return new NextResponse(getUnsubscribeHtml(true, 'You have been successfully unsubscribed from the Heavy Metal Stats daily bulletin.'), {
        status: 200,
        headers: { 'Content-Type': 'text/html' },
      });
    } else {
      return new NextResponse(getUnsubscribeHtml(false, 'This link has already been used or is no longer valid.'), {
        status: 
Repobility — the code-quality scanner for AI-generated software · https://repobility.com
getUnsubscribeHtml function · typescript · L45-L121 (77 LOC)
app/api/newsletter/unsubscribe/route.ts
function getUnsubscribeHtml(success: boolean, message: string): string {
  const icon = success ? '&#10003;' : '&#10007;';
  const iconColor = success ? '#22c55e' : '#ef4444';
  const title = success ? 'Unsubscribed' : 'Error';

  return `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${title} — Heavy Metal Stats</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      background-color: #0f172a;
      font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
      padding: 20px;
    }
    .card {
      background: #1e293b;
      border-radius: 12px;
      padding: 48px 40px;
      max-width: 480px;
      text-align: center;
    }
    .icon {
      width: 64px;
      height: 64px;
      border-radius: 50%;
      display: flex;
      align-items: 
GET function · typescript · L8-L46 (39 LOC)
app/api/paper-physical/[metal]/history/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ metal: string }> }
) {
  try {
    if (!isDatabaseAvailable()) {
      return NextResponse.json(
        { success: false, error: 'Database not configured' },
        { status: 503 }
      );
    }

    const { metal } = await params;
    const { searchParams } = new URL(request.url);
    const days = parseInt(searchParams.get('days') || '90', 10);

    const history = await getPaperPhysicalHistory(metal, days);

    return NextResponse.json({
      success: true,
      metal,
      days,
      data: history.map(h => ({
        date: h.report_date,
        openInterest: Number(h.open_interest),
        openInterestUnits: Number(h.open_interest_units),
        registeredInventory: Number(h.registered_inventory),
        paperPhysicalRatio: Number(h.paper_physical_ratio),
        riskLevel: h.risk_level,
      })),
    });
  } catch (error) {
    console.error('Error fetching paper/physical history:', er
GET function · typescript · L12-L83 (72 LOC)
app/api/paper-physical/route.ts
export async function GET() {
  try {
    if (!isDatabaseAvailable()) {
      return NextResponse.json(
        { success: false, error: 'Database not configured' },
        { status: 503 }
      );
    }

    const [paperPhysicalRatios, openInterestData] = await Promise.all([
      getLatestPaperPhysicalRatios(),
      getLatestOpenInterest(),
    ]);

    // Format the response
    const ratiosByMetal: Record<string, {
      metal: string;
      reportDate: string;
      futuresSymbol: string;
      openInterest: number;
      openInterestUnits: number;
      registeredInventory: number;
      paperPhysicalRatio: number;
      riskLevel: string;
    }> = {};

    for (const ratio of paperPhysicalRatios) {
      ratiosByMetal[ratio.metal] = {
        metal: ratio.metal,
        reportDate: ratio.report_date,
        futuresSymbol: ratio.futures_symbol,
        openInterest: Number(ratio.open_interest),
        openInterestUnits: Number(ratio.open_interest_units),
        registeredInv
getRiskLevel function · typescript · L21-L26 (6 LOC)
app/api/paper-physical/sync/route.ts
function getRiskLevel(ratio: number): string {
  if (ratio <= 2) return 'LOW';
  if (ratio <= 5) return 'MODERATE';
  if (ratio <= 10) return 'HIGH';
  return 'EXTREME';
}
POST function · typescript · L29-L151 (123 LOC)
app/api/paper-physical/sync/route.ts
export async function POST(request: Request) {
  if (!isAuthorized(request)) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  try {
    if (!isDatabaseAvailable()) {
      return NextResponse.json(
        { success: false, error: 'Database not configured' },
        { status: 503 }
      );
    }

    // Initialize tables if they don't exist
    await initializeOpenInterestTables();

    // Read volume summary data (contains open interest)
    const volumeSummaryPath = path.join(process.cwd(), 'public', 'volume_summary.json');
    const volumeSummaryRaw = await fs.readFile(volumeSummaryPath, 'utf-8');
    const volumeSummary = JSON.parse(volumeSummaryRaw);

    // Read inventory data (contains registered inventory)
    const dataPath = path.join(process.cwd(), 'public', 'data.json');
    const dataRaw = await fs.readFile(dataPath, 'utf-8');
    const inventoryData = JSON.parse(dataRaw);

    const reportDate = volumeSummary.parsed_date || new Date().t
GET function · typescript · L154-L178 (25 LOC)
app/api/paper-physical/sync/route.ts
export async function GET() {
  try {
    if (!isDatabaseAvailable()) {
      return NextResponse.json(
        { success: false, error: 'Database not configured' },
        { status: 503 }
      );
    }

    // Initialize tables to ensure they exist
    await initializeOpenInterestTables();

    return NextResponse.json({
      success: true,
      message: 'Paper/physical tables ready',
      tables: ['open_interest_snapshots', 'paper_physical_snapshots'],
    });
  } catch (error) {
    console.error('Paper/physical status error:', error);
    return NextResponse.json(
      { success: false, error: 'Failed to check paper/physical status' },
      { status: 500 }
    );
  }
}
GET function · typescript · L6-L37 (32 LOC)
app/api/prices/route.ts
export async function GET() {
  try {
    if (!isDatabaseAvailable()) {
      return NextResponse.json(
        { success: false, error: 'Database not available' },
        { status: 503 },
      );
    }

    const cached = await getCachedPrices('spot');
    if (!cached) {
      return NextResponse.json(
        { success: false, error: 'No cached data yet — cron has not run' },
        { status: 503 },
      );
    }

    const { prices, fetched_at } = cached.data as { prices: Record<string, unknown>; fetched_at: string };

    return NextResponse.json(
      { success: true, prices, fetched_at },
      {
        headers: {
          'Cache-Control': 'public, s-maxage=10, stale-while-revalidate=60',
        },
      },
    );
  } catch (error: unknown) {
    const msg = error instanceof Error ? error.message : 'Unknown error';
    return NextResponse.json({ success: false, error: msg }, { status: 500 });
  }
}
GET function · typescript · L7-L53 (47 LOC)
app/api/risk-score/[metal]/history/route.ts
export async function GET(
  request: Request,
  { params }: { params: Promise<{ metal: string }> }
) {
  try {
    const { metal } = await params;
    
    if (!isDatabaseAvailable()) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    const { searchParams } = new URL(request.url);
    const days = parseInt(searchParams.get('days') || '90', 10);

    const history = await getRiskScoreHistory(metal, days);

    return NextResponse.json({
      success: true,
      metal,
      days,
      count: history.length,
      history: history.map(h => ({
        date: h.report_date,
        composite: h.composite_score,
        level: h.risk_level,
        breakdown: {
          coverageRisk: h.coverage_risk,
          paperPhysicalRisk: h.paper_physical_risk,
          inventoryTrendRisk: h.inventory_trend_risk,
          deliveryVelocityRisk: h.delivery_velocity_risk,
          marketActivityRisk: h.market_activity_risk,
Repobility · severity-and-effort ranking · https://repobility.com
GET function · typescript · L7-L63 (57 LOC)
app/api/risk-score/route.ts
export async function GET() {
  try {
    if (!isDatabaseAvailable()) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    const riskScores = await getLatestRiskScores();

    // Transform to a more usable format
    const scores: Record<string, {
      composite: number;
      level: string;
      breakdown: {
        coverageRisk: number;
        paperPhysicalRisk: number;
        inventoryTrendRisk: number;
        deliveryVelocityRisk: number;
        marketActivityRisk: number;
      };
      dominantFactor: string;
      commentary: string;
      reportDate: string;
    }> = {};

    for (const score of riskScores) {
      scores[score.metal] = {
        composite: score.composite_score,
        level: score.risk_level,
        breakdown: {
          coverageRisk: score.coverage_risk,
          paperPhysicalRisk: score.paper_physical_risk,
          inventoryTrendRisk: score.inventory_trend_risk,
          del
POST function · typescript · L36-L147 (112 LOC)
app/api/risk-score/sync/route.ts
export async function POST(request: Request) {
  if (!isAuthorized(request)) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  try {
    if (!isDatabaseAvailable()) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    // Initialize tables
    await initializeRiskScoreTables();

    // Read data files
    const publicDir = path.join(process.cwd(), 'public');
    
    const dataJson = await fs.readFile(path.join(publicDir, 'data.json'), 'utf-8');
    const warehouseData: WarehouseStocksData = JSON.parse(dataJson);
    
    const volumeJson = await fs.readFile(path.join(publicDir, 'volume_summary.json'), 'utf-8');
    const volumeData: VolumeSummaryData = JSON.parse(volumeJson);

    const reportDate = volumeData.date || new Date().toISOString().split('T')[0];
    const results: Array<{ metal: string; score: number; level: string }> = [];

    // Calculate and store risk scores for each
getStripe function · typescript · L5-L9 (5 LOC)
app/api/stripe/api-checkout/route.ts
function getStripe() {
  return new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: '2026-01-28.clover',
  });
}
GET function · typescript · L12-L49 (38 LOC)
app/api/stripe/api-checkout/route.ts
export async function GET() {
  try {
    const session = await auth();
    if (!session?.user?.id || !session.user.email) {
      return NextResponse.redirect(new URL('/auth/login', 'https://heavymetalstats.com'));
    }

    const priceId = process.env.STRIPE_API_PRICE_ID;
    if (!priceId) {
      console.error('STRIPE_API_PRICE_ID not set');
      return NextResponse.json({ error: 'Payment configuration error' }, { status: 500 });
    }

    const stripe = getStripe();

    const checkoutSession = await stripe.checkout.sessions.create({
      mode: 'subscription',
      payment_method_types: ['card'],
      customer_email: session.user.email.toLowerCase().trim(),
      line_items: [{ price: priceId, quantity: 1 }],
      success_url: 'https://heavymetalstats.com/developer?upgraded=true',
      cancel_url: 'https://heavymetalstats.com/developer?upgraded=cancelled',
      metadata: {
        product_type: 'api',
        user_id: session.user.id,
      },
    });

    if (checkoutSess
getStripe function · typescript · L6-L10 (5 LOC)
app/api/stripe/checkout-redirect/route.ts
function getStripe() {
  return new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: '2026-01-28.clover',
  });
}
isValidEmail function · typescript · L13-L15 (3 LOC)
app/api/stripe/checkout-redirect/route.ts
function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
GET function · typescript · L18-L79 (62 LOC)
app/api/stripe/checkout-redirect/route.ts
export async function GET(request: Request) {
  // Rate limit: 10 checkout attempts per IP per 15 minutes
  const ip = getClientIp(request);
  const { limited } = rateLimit(`checkout:${ip}`, 10, 15 * 60 * 1000);
  if (limited) {
    return NextResponse.redirect('https://heavymetalstats.com?error=rate-limited');
  }

  const stripe = getStripe();
  const { searchParams } = new URL(request.url);
  const email = searchParams.get('email');

  if (!email || !isValidEmail(email)) {
    return NextResponse.redirect('https://heavymetalstats.com?error=missing-email');
  }

  if (!isDatabaseAvailable()) {
    return NextResponse.redirect('https://heavymetalstats.com?error=unavailable');
  }

  try {
    const subscriber = await getSubscriberByEmail(email);
    if (!subscriber) {
      return NextResponse.redirect('https://heavymetalstats.com?error=not-found');
    }

    if (subscriber.subscription_status === 'paid') {
      return NextResponse.redirect('https://heavymetalstats.com?subscribed=al
getStripe function · typescript · L5-L9 (5 LOC)
app/api/stripe/checkout/route.ts
function getStripe() {
  return new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: '2026-01-28.clover',
  });
}
Source: Repobility analyzer · https://repobility.com
POST function · typescript · L11-L65 (55 LOC)
app/api/stripe/checkout/route.ts
export async function POST(request: Request) {
  try {
    const stripe = getStripe();
    const body = await request.json();
    const { email } = body;

    if (!email) {
      return NextResponse.json({ error: 'Email is required' }, { status: 400 });
    }

    if (!isDatabaseAvailable()) {
      return NextResponse.json({ error: 'Service unavailable' }, { status: 503 });
    }

    // Verify subscriber exists
    const subscriber = await getSubscriberByEmail(email);
    if (!subscriber) {
      return NextResponse.json({ error: 'No subscription found for this email. Please sign up first.' }, { status: 404 });
    }

    // If already paid, no need for checkout
    if (subscriber.subscription_status === 'paid') {
      return NextResponse.json({ error: 'You already have an active subscription.' }, { status: 400 });
    }

    const priceId = process.env.STRIPE_PRICE_ID;
    if (!priceId) {
      console.error('STRIPE_PRICE_ID not set');
      return NextResponse.json({ error: 'Payme
getStripe function · typescript · L12-L16 (5 LOC)
app/api/stripe/webhook/route.ts
function getStripe() {
  return new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: '2026-01-28.clover',
  });
}
getResend function · typescript · L18-L20 (3 LOC)
app/api/stripe/webhook/route.ts
function getResend() {
  return new Resend(process.env.RESEND_API_KEY);
}
POST function · typescript · L22-L152 (131 LOC)
app/api/stripe/webhook/route.ts
export async function POST(request: Request) {
  const stripe = getStripe();
  const body = await request.text();
  const signature = request.headers.get('stripe-signature');

  if (!signature) {
    return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
  }

  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
  if (!webhookSecret) {
    console.error('STRIPE_WEBHOOK_SECRET not set');
    return NextResponse.json({ error: 'Webhook not configured' }, { status: 500 });
  }

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
  } catch (err) {
    console.error('Webhook signature verification failed:', err);
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
  }

  try {
    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object as Stripe.Checkout.Session;

        // Handle API Pro subscription
        if (session.metadata?
buildPaymentConfirmationEmail function · typescript · L154-L230 (77 LOC)
app/api/stripe/webhook/route.ts
function buildPaymentConfirmationEmail(isNew: boolean, unsubscribeUrl: string): string {
  const heading = isNew
    ? 'Welcome to Heavy Metal Stats!'
    : 'Payment Confirmed';
  const body = isNew
    ? 'Your subscription is now active. You\'ll receive the Daily Bulletin Analysis every trading day with COMEX risk scores, warehouse data, and market insights delivered to your inbox.'
    : 'Your subscription has been renewed. You\'ll continue receiving the Daily Bulletin Analysis every trading day.';

  return `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin:0;padding:0;background-color:#0f172a;font-family:'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
  <table width="100%" cellpadding="0" cellspacing="0" style="background-color:#0f172a;padding:40px 20px;">
    <tr>
      <td align="center">
        <table width="600" cellpadding="0" cellspacing="0" style="background-
GET function · typescript · L7-L52 (46 LOC)
app/api/v1/forecast/accuracy/route.ts
export async function GET(request: Request) {
  const auth = await validateApiKey(request);
  if (!auth) {
    return NextResponse.json(
      { success: false, error: 'Invalid or missing API key.' },
      { status: 401 }
    );
  }

  if (auth.tier !== 'paid') {
    return NextResponse.json(
      { success: false, error: 'Forecast accuracy data requires a paid API plan.' },
      { status: 403 }
    );
  }

  const rl = await checkApiRateLimit(auth.keyId, auth.tier);
  if (rl) {
    return NextResponse.json(
      { success: false, error: rl.error },
      { status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
    );
  }

  await trackUsage(auth.keyId);

  try {
    if (!isDatabaseAvailable()) {
      return NextResponse.json({ success: false, error: 'Service unavailable' }, { status: 503 });
    }

    const { searchParams } = new URL(request.url);
    const days = Math.min(parseInt(searchParams.get('days') || '90', 10), 365);

    const summ
GET function · typescript · L10-L74 (65 LOC)
app/api/v1/forecast/[metal]/route.ts
export async function GET(
  request: Request,
  { params }: { params: Promise<{ metal: string }> }
) {
  const auth = await validateApiKey(request);
  if (!auth) {
    return NextResponse.json(
      { success: false, error: 'Invalid or missing API key.' },
      { status: 401 }
    );
  }

  if (auth.tier !== 'paid') {
    return NextResponse.json(
      { success: false, error: 'Per-metal forecast data requires a paid API plan. Use GET /api/v1/forecast for the free tier.' },
      { status: 403 }
    );
  }

  const rl = await checkApiRateLimit(auth.keyId, auth.tier);
  if (rl) {
    return NextResponse.json(
      { success: false, error: rl.error },
      { status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
    );
  }

  await trackUsage(auth.keyId);

  try {
    const { metal } = await params;
    const normalized = metal.toLowerCase();

    if (!VALID_METALS.includes(normalized)) {
      return NextResponse.json(
        { success: false
GET function · typescript · L8-L41 (34 LOC)
app/api/v1/forecast/route.ts
export async function GET(request: Request) {
  const auth = await validateApiKey(request);
  if (!auth) {
    return NextResponse.json(
      { success: false, error: 'Invalid or missing API key.' },
      { status: 401 }
    );
  }

  const rl = await checkApiRateLimit(auth.keyId, auth.tier);
  if (rl) {
    return NextResponse.json(
      { success: false, error: rl.error },
      { status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
    );
  }

  await trackUsage(auth.keyId);

  try {
    const filePath = path.join(process.cwd(), 'public', 'forecast.json');
    const raw = await fs.readFile(filePath, 'utf-8');
    const data = JSON.parse(raw);

    return NextResponse.json({
      success: true,
      data,
      meta: { fetched_at: new Date().toISOString(), tier: auth.tier },
    });
  } catch (error) {
    console.error('[API v1] Error reading forecast data:', error);
    return NextResponse.json({ success: false, error: 'Forecast data not
Powered by Repobility — scan your code at https://repobility.com
GET function · typescript · L7-L68 (62 LOC)
app/api/v1/paper-physical/[metal]/route.ts
export async function GET(
  request: Request,
  { params }: { params: Promise<{ metal: string }> }
) {
  const auth = await validateApiKey(request);
  if (!auth) {
    return NextResponse.json(
      { success: false, error: 'Invalid or missing API key.' },
      { status: 401 }
    );
  }

  if (auth.tier !== 'paid') {
    return NextResponse.json(
      { success: false, error: 'Historical paper/physical data requires a paid API plan.' },
      { status: 403 }
    );
  }

  const rl = await checkApiRateLimit(auth.keyId, auth.tier);
  if (rl) {
    return NextResponse.json(
      { success: false, error: rl.error },
      { status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
    );
  }

  await trackUsage(auth.keyId);

  try {
    if (!isDatabaseAvailable()) {
      return NextResponse.json({ success: false, error: 'Service unavailable' }, { status: 503 });
    }

    const { metal } = await params;
    const { searchParams } = new URL(request
GET function · typescript · L7-L56 (50 LOC)
app/api/v1/paper-physical/route.ts
export async function GET(request: Request) {
  const auth = await validateApiKey(request);
  if (!auth) {
    return NextResponse.json(
      { success: false, error: 'Invalid or missing API key.' },
      { status: 401 }
    );
  }

  const rl = await checkApiRateLimit(auth.keyId, auth.tier);
  if (rl) {
    return NextResponse.json(
      { success: false, error: rl.error },
      { status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
    );
  }

  await trackUsage(auth.keyId);

  try {
    if (!isDatabaseAvailable()) {
      return NextResponse.json({ success: false, error: 'Service unavailable' }, { status: 503 });
    }

    const ratios = await getLatestPaperPhysicalRatios();

    const data: Record<string, unknown> = {};
    for (const ratio of ratios) {
      data[ratio.metal] = {
        metal: ratio.metal,
        reportDate: ratio.report_date,
        futuresSymbol: ratio.futures_symbol,
        openInterest: Number(ratio.open_interes
GET function · typescript · L7-L74 (68 LOC)
app/api/v1/risk-scores/[metal]/route.ts
export async function GET(
  request: Request,
  { params }: { params: Promise<{ metal: string }> }
) {
  const auth = await validateApiKey(request);
  if (!auth) {
    return NextResponse.json(
      { success: false, error: 'Invalid or missing API key.' },
      { status: 401 }
    );
  }

  if (auth.tier !== 'paid') {
    return NextResponse.json(
      { success: false, error: 'Historical risk score data requires a paid API plan.' },
      { status: 403 }
    );
  }

  const rl = await checkApiRateLimit(auth.keyId, auth.tier);
  if (rl) {
    return NextResponse.json(
      { success: false, error: rl.error },
      { status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
    );
  }

  await trackUsage(auth.keyId);

  try {
    if (!isDatabaseAvailable()) {
      return NextResponse.json({ success: false, error: 'Service unavailable' }, { status: 503 });
    }

    const { metal } = await params;
    const { searchParams } = new URL(request.url
GET function · typescript · L7-L60 (54 LOC)
app/api/v1/risk-scores/route.ts
export async function GET(request: Request) {
  const auth = await validateApiKey(request);
  if (!auth) {
    return NextResponse.json(
      { success: false, error: 'Invalid or missing API key. Pass via Authorization: Bearer hms_... header or ?apikey= parameter.' },
      { status: 401 }
    );
  }

  const rl = await checkApiRateLimit(auth.keyId, auth.tier);
  if (rl) {
    return NextResponse.json(
      { success: false, error: rl.error },
      { status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
    );
  }

  await trackUsage(auth.keyId);

  try {
    if (!isDatabaseAvailable()) {
      return NextResponse.json({ success: false, error: 'Service unavailable' }, { status: 503 });
    }

    const riskScores = await getLatestRiskScores();

    const scores: Record<string, unknown> = {};
    for (const score of riskScores) {
      scores[score.metal] = {
        composite: score.composite_score,
        level: score.risk_level,
        brea
LoginPage function · typescript · L15-L41 (27 LOC)
app/auth/login/page.tsx
export default function LoginPage() {
  const router = useRouter();

  useEffect(() => {
    if (process.env.NODE_ENV === 'production') {
      router.replace('/');
    }
  }, [router]);

  if (process.env.NODE_ENV === 'production') {
    return (
      <div className="min-h-screen bg-slate-50 dark:bg-slate-950 flex items-center justify-center">
        <Loader2 className="w-6 h-6 text-amber-500 animate-spin" />
      </div>
    );
  }

  return (
    <Suspense fallback={
      <div className="min-h-screen bg-slate-50 dark:bg-slate-950 flex items-center justify-center">
        <Loader2 className="w-6 h-6 text-amber-500 animate-spin" />
      </div>
    }>
      <LoginForm />
    </Suspense>
  );
}
LoginForm function · typescript · L43-L225 (183 LOC)
app/auth/login/page.tsx
function LoginForm() {
  const searchParams = useSearchParams();
  const rawCallback = searchParams.get('callbackUrl') || '/discuss';
  // Prevent open redirect: only allow relative paths on the same origin
  const callbackUrl = rawCallback.startsWith('/') && !rawCallback.startsWith('//') ? rawCallback : '/discuss';
  const errorParam = searchParams.get('error');

  // Map known error codes to safe messages instead of displaying raw URL params
  const ERROR_MESSAGES: Record<string, string> = {
    CredentialsSignin: 'Invalid email or password.',
    OAuthSignin: 'Could not start sign-in. Please try again.',
    OAuthCallback: 'Sign-in callback failed. Please try again.',
    OAuthAccountNotLinked: 'This email is already linked to another provider.',
    SessionRequired: 'Please sign in to continue.',
    Default: 'An authentication error occurred. Please try again.',
  };

  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoa
handleCredentialsLogin function · typescript · L68-L91 (24 LOC)
app/auth/login/page.tsx
  async function handleCredentialsLogin(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      const result = await signIn('credentials', {
        email: email.toLowerCase().trim(),
        password,
        callbackUrl,
        redirect: false,
      });

      if (result?.error) {
        setError('Invalid email or password.');
        setLoading(false);
      } else {
        window.location.href = callbackUrl;
      }
    } catch {
      setError('Something went wrong. Please try again.');
      setLoading(false);
    }
  }
handleOAuthLogin function · typescript · L93-L96 (4 LOC)
app/auth/login/page.tsx
  function handleOAuthLogin(providerId: string) {
    setOauthLoading(providerId);
    signIn(providerId, { callbackUrl });
  }
Repobility — the code-quality scanner for AI-generated software · https://repobility.com
GoogleIcon function · typescript · L227-L236 (10 LOC)
app/auth/login/page.tsx
function GoogleIcon({ className }: { className?: string }) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="currentColor">
      <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" />
      <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
      <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
      <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
    </svg>
  );
}
GitHubIcon function · typescript · L238-L244 (7 LOC)
app/auth/login/page.tsx
function GitHubIcon({ className }: { className?: string }) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="currentColor">
      <path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z" />
    </svg>
  );
}
DiscordIcon function · typescript · L246-L252 (7 LOC)
app/auth/login/page.tsx
function DiscordIcon({ className }: { className?: string }) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="currentColor">
      <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.15
‹ prevpage 2 / 13next ›