← 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 });
}
}