← back to Patty

app/api/orbit/route.ts

159 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { verifyAuth } from '@/lib/auth';
import { auditLog } from '@/lib/audit';

const KEN_API = 'http://127.0.0.1:7810/api/markets';

/**
 * GET /api/orbit
 * Orbit dashboard: sync Kalshi markets, return markets + petitions + links + stats
 */
export async function GET(request: NextRequest) {
  const user = verifyAuth(request);
  if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

  try {
    // 1. Fetch fresh markets from Ken/Kalshi agent
    let kenMarkets: any[] = [];
    try {
      const kenRes = await fetch(KEN_API, { signal: AbortSignal.timeout(8000) });
      if (kenRes.ok) {
        const kenData = await kenRes.json();
        kenMarkets = kenData.markets || [];
      } else {
        console.warn('[orbit] Ken API returned', kenRes.status);
      }
    } catch (err) {
      console.warn('[orbit] Ken API unreachable:', (err as Error).message);
    }

    // 2. Upsert markets into orbit_markets
    let upsertCount = 0;
    for (const m of kenMarkets) {
      try {
        await query(
          `INSERT INTO orbit_markets (ticker, event_ticker, title, subtitle, category,
            yes_bid, yes_ask, no_bid, no_ask, last_price, volume, volume_24h, open_interest,
            raw_data, last_synced_at)
           VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,NOW())
           ON CONFLICT (ticker) DO UPDATE SET
             event_ticker = EXCLUDED.event_ticker,
             title = EXCLUDED.title,
             subtitle = EXCLUDED.subtitle,
             category = EXCLUDED.category,
             yes_bid = EXCLUDED.yes_bid,
             yes_ask = EXCLUDED.yes_ask,
             no_bid = EXCLUDED.no_bid,
             no_ask = EXCLUDED.no_ask,
             last_price = EXCLUDED.last_price,
             volume = EXCLUDED.volume,
             volume_24h = EXCLUDED.volume_24h,
             open_interest = EXCLUDED.open_interest,
             raw_data = EXCLUDED.raw_data,
             last_synced_at = NOW(),
             updated_at = NOW()`,
          [
            m.ticker,
            m.event_ticker || null,
            m.title,
            m.subtitle || null,
            m.category || null,
            m.yes_bid ?? null,
            m.yes_ask ?? null,
            m.no_bid ?? null,
            m.no_ask ?? null,
            m.last_price ?? null,
            m.volume ? parseInt(m.volume, 10) : null,
            m.volume_24h ? parseInt(m.volume_24h, 10) : null,
            m.open_interest ? parseInt(m.open_interest, 10) : null,
            JSON.stringify(m),
          ],
        );
        upsertCount++;
      } catch (err) {
        console.error('[orbit] Upsert failed for', m.ticker, (err as Error).message);
      }
    }

    // 3. Fetch data for response
    const [marketsRes, petitionsRes, linksRes, feedsRes, articlesRes, trendingRes] = await Promise.all([
      query(`SELECT * FROM orbit_markets ORDER BY petition_relevance DESC, last_synced_at DESC LIMIT 200`),
      query(`SELECT id, title, slug, status, category, signature_count, signature_goal, ai_source, created_at
             FROM petitions ORDER BY created_at DESC LIMIT 50`),
      query(`SELECT ol.*, om.ticker, om.title as market_title, p.title as petition_title
             FROM orbit_links ol
             JOIN orbit_markets om ON ol.market_id = om.id
             JOIN petitions p ON ol.petition_id = p.id
             ORDER BY ol.created_at DESC LIMIT 100`),
      query(`SELECT COUNT(*)::int as count FROM orbit_feeds WHERE is_active = true`),
      query(`SELECT COUNT(*)::int as count FROM orbit_articles`),
      query(`SELECT id, title, content, category, tags, engagement_score, source_url, created_at
             FROM trending_topics ORDER BY engagement_score DESC LIMIT 15`),
    ]);

    const stats = {
      total_markets: marketsRes.rows.length,
      synced_from_ken: upsertCount,
      linked_petitions: linksRes.rows.length,
      active_feeds: feedsRes.rows[0]?.count || 0,
      recent_articles: articlesRes.rows[0]?.count || 0,
    };

    return NextResponse.json({
      markets: marketsRes.rows,
      petitions: petitionsRes.rows,
      links: linksRes.rows,
      trending: trendingRes.rows,
      stats,
    });
  } catch (err) {
    console.error('[api/orbit] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch orbit data' }, { status: 500 });
  }
}

/**
 * POST /api/orbit
 * Create a market-petition link
 */
export async function POST(request: NextRequest) {
  const user = verifyAuth(request);
  if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

  try {
    const body = await request.json();
    const { market_id, petition_id, link_type, strength, ai_reasoning } = body;

    if (!market_id || !petition_id) {
      return NextResponse.json({ error: 'market_id and petition_id are required' }, { status: 400 });
    }

    const validLinkTypes = ['influence', 'counter', 'support'];
    const type = validLinkTypes.includes(link_type) ? link_type : 'influence';
    const str = typeof strength === 'number' ? Math.max(0, Math.min(1, strength)) : 0.5;

    const result = await query(
      `INSERT INTO orbit_links (market_id, petition_id, link_type, strength, ai_reasoning)
       VALUES ($1, $2, $3, $4, $5)
       ON CONFLICT (market_id, petition_id) DO UPDATE SET
         link_type = EXCLUDED.link_type,
         strength = EXCLUDED.strength,
         ai_reasoning = EXCLUDED.ai_reasoning
       RETURNING *`,
      [market_id, petition_id, type, str, ai_reasoning || null],
    );

    await auditLog('orbit.link_created', 'orbit_link', result.rows[0].id, {
      market_id,
      petition_id,
      link_type: type,
    });

    return NextResponse.json({ link: result.rows[0] }, { status: 201 });
  } catch (err) {
    console.error('[api/orbit] POST error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to create orbit link' }, { status: 500 });
  }
}