← back to Patty
app/api/orbit/batch-link/route.ts
169 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { verifyAuth } from '@/lib/auth';
import { callGemini } from '@/lib/gemini';
export async function POST(request: NextRequest) {
const user = verifyAuth(request);
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
try {
// Fetch top markets, petitions, and trending topics
const [marketsRes, petitionsRes, trendingRes] = await Promise.all([
query(`SELECT id, ticker, title, category, yes_bid, volume, petition_relevance
FROM orbit_markets ORDER BY volume DESC NULLS LAST LIMIT 25`),
query(`SELECT id, title, category, status FROM petitions ORDER BY created_at DESC LIMIT 20`),
query(`SELECT id, title, content, category, tags FROM trending_topics ORDER BY engagement_score DESC LIMIT 15`),
]);
const markets = marketsRes.rows;
const petitions = petitionsRes.rows;
const trending = trendingRes.rows;
if (markets.length === 0) {
return NextResponse.json({ error: 'No markets available to link' }, { status: 400 });
}
const marketSummary = markets.map((m, i) =>
`M${i + 1}. [${m.ticker}] "${m.title}" (${m.category}, ${m.yes_bid}% yes, vol=${m.volume})`
).join('\n');
const petitionSummary = petitions.length > 0
? petitions.map((p, i) => `P${i + 1}. "${p.title}" (${p.category}, ${p.status})`).join('\n')
: 'No petitions yet.';
const trendingSummary = trending.length > 0
? trending.map((t, i) => `T${i + 1}. "${t.title}" — ${(t.content || '').substring(0, 100)}`).join('\n')
: 'No trending topics.';
const prompt = `You are an AI analyst connecting prediction markets, petitions, and trending topics for an advocacy platform.
MARKETS:
${marketSummary}
PETITIONS:
${petitionSummary}
TRENDING TOPICS:
${trendingSummary}
Generate connections between these items. For each connection, provide:
1. market_index (M1, M2...) and petition_index (P1, P2...) OR trending_index (T1, T2...)
2. link_type: "influence" (market affects petition), "counter" (opposing), "support" (aligned), or "alliance" (unexpected connection)
3. strength: 0.0 to 1.0 (how strong is the connection)
4. ai_reasoning: one sentence explaining the connection
Also suggest NEW petition ideas from trending topics that don't have petitions yet.
Return a JSON object:
{
"links": [
{ "market_idx": "M1", "petition_idx": "P1", "link_type": "influence", "strength": 0.8, "ai_reasoning": "..." },
{ "market_idx": "M3", "trending_idx": "T2", "link_type": "support", "strength": 0.7, "ai_reasoning": "..." }
],
"suggested_petitions": [
{ "trending_idx": "T1", "title": "...", "summary": "...", "suggested_target": "...", "category": "..." }
]
}
Generate 15-25 links and 3-5 petition suggestions. Only include strong, meaningful connections (strength > 0.4).
Return ONLY valid JSON, no markdown.`;
type BatchLinkResult = {
links: Array<{
market_idx: string;
petition_idx?: string;
trending_idx?: string;
link_type: string;
strength: number;
ai_reasoning: string;
}>;
suggested_petitions: Array<{
trending_idx: string;
title: string;
summary: string;
suggested_target: string;
category: string;
}>;
};
// 2026-05-05 (tick 29): migrated to lib/gemini.ts wrapper.
const r = await callGemini<BatchLinkResult>({ prompt, maxTokens: 4096, temperature: 0.7 });
if (!r.ok) {
console.error('[batch-link] gemini failed:', r.reason, r.detail);
return NextResponse.json(
{ error: r.reason === 'no_key' ? 'AI service not configured' : 'AI analysis failed' },
{ status: r.status },
);
}
const result = r.data;
// Insert market-petition links
let linksInserted = 0;
for (const link of (result.links || [])) {
const mIdx = parseInt(link.market_idx?.replace('M', '')) - 1;
const market = markets[mIdx];
if (!market) continue;
// Link to petition
if (link.petition_idx) {
const pIdx = parseInt(link.petition_idx.replace('P', '')) - 1;
const petition = petitions[pIdx];
if (!petition) continue;
try {
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 = $3, strength = $4, ai_reasoning = $5
`, [market.id, petition.id, link.link_type || 'influence', link.strength || 0.5, link.ai_reasoning || '']);
linksInserted++;
} catch (e) {
console.error('[api/orbit/batch-link] Link insert error:', (e as Error).message);
}
}
}
// Create suggested petitions from trending topics
let petitionsCreated = 0;
for (const sp of (result.suggested_petitions || [])) {
const tIdx = parseInt(sp.trending_idx?.replace('T', '')) - 1;
const topic = trending[tIdx];
if (!topic || !sp.title) continue;
try {
await query(`
INSERT INTO petitions (title, summary, category, status, target_name)
VALUES ($1, $2, $3, 'draft', $4)
`, [sp.title, sp.summary || '', sp.category || 'policy', sp.suggested_target || '']);
petitionsCreated++;
} catch (e) {
console.error('[api/orbit/batch-link] Petition insert error:', (e as Error).message);
}
}
// Update petition_relevance on markets
for (const link of (result.links || [])) {
const mIdx = parseInt(link.market_idx?.replace('M', '')) - 1;
const market = markets[mIdx];
if (!market) continue;
const relevance = Math.round((link.strength || 0.5) * 100);
try {
await query(`UPDATE orbit_markets SET petition_relevance = GREATEST(petition_relevance, $1) WHERE id = $2`,
[relevance, market.id]);
} catch { /* ignore */ }
}
return NextResponse.json({
links_inserted: linksInserted,
petitions_created: petitionsCreated,
total_links_analyzed: (result.links || []).length,
total_suggestions: (result.suggested_petitions || []).length,
});
} catch (err) {
console.error('[api/orbit/batch-link] Error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to generate links' }, { status: 500 });
}
}