← back to Patty
app/api/orbit/suggest-alliance/route.ts
205 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { verifyAuth } from '@/lib/auth';
import { auditLog } from '@/lib/audit';
import { callGemini } from '@/lib/gemini';
/**
* POST /api/orbit/suggest-alliance
* Generate an "unlikely alliance" petition from 2 Kalshi markets in different categories.
* The AI finds common ground between seemingly opposed groups for maximum political impact.
*/
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_tickers, tone } = body;
if (!market_tickers || !Array.isArray(market_tickers) || market_tickers.length !== 2) {
return NextResponse.json({ error: 'market_tickers must be an array of exactly 2 tickers' }, { status: 400 });
}
// Fetch both markets
const [res1, res2] = await Promise.all([
query(`SELECT * FROM orbit_markets WHERE ticker = $1`, [market_tickers[0]]),
query(`SELECT * FROM orbit_markets WHERE ticker = $1`, [market_tickers[1]]),
]);
if (res1.rows.length === 0 || res2.rows.length === 0) {
return NextResponse.json({ error: 'One or both markets not found. Sync markets first.' }, { status: 404 });
}
const marketA = res1.rows[0];
const marketB = res2.rows[0];
if (marketA.category === marketB.category) {
return NextResponse.json({ error: 'Alliance petitions require markets from different categories.' }, { status: 400 });
}
const toneDesc = tone || 'urgent';
const prompt = `You are a master political strategist and coalition builder. Your specialty is finding UNLIKELY ALLIANCES between groups that normally oppose each other. When these groups unite on a specific issue, the political impact is 10x greater than either group alone — this is the "Baptists and bootleggers" principle.
MARKET A (${marketA.category}):
- Ticker: ${marketA.ticker}
- Title: ${marketA.title}
- Subtitle: ${marketA.subtitle || 'N/A'}
- Category: ${marketA.category}
- Probability: ${marketA.last_price || marketA.yes_bid || 'N/A'}%
- Volume: ${marketA.volume || 'N/A'} contracts
MARKET B (${marketB.category}):
- Ticker: ${marketB.ticker}
- Title: ${marketB.title}
- Subtitle: ${marketB.subtitle || 'N/A'}
- Category: ${marketB.category}
- Probability: ${marketB.last_price || marketB.yes_bid || 'N/A'}%
- Volume: ${marketB.volume || 'N/A'} contracts
YOUR TASK:
Find the INTERSECTION where supporters of both issues can agree on ONE petition.
The petition should feel authentic to BOTH audiences. Frame it around shared values
(freedom, fairness, safety, economic opportunity) rather than partisan language.
Tone: ${toneDesc} (${toneDesc === 'urgent' ? 'time-sensitive, action-oriented' : toneDesc === 'formal' ? 'professional, data-driven' : toneDesc === 'passionate' ? 'emotional, empathetic' : 'practical, solution-focused'})
RULES:
- Do NOT mention Kalshi, prediction markets, or betting
- The petition must appeal genuinely to BOTH groups
- Find a policy angle where both groups' interests align
- Use specific, actionable demands
Return a JSON object with EXACTLY these fields:
{
"title": "Alliance petition title (max 100 chars)",
"summary": "2-3 sentence hook explaining why these groups should unite (max 300 chars)",
"body_html": "<p>Full petition body in HTML. 5-7 paragraphs. Address both constituencies directly. Include specific demands, shared values, urgency, and a unified call to action. Use <strong>, <em>, <ul>, <li> tags.</p>",
"body_text": "Plain text version of the body",
"suggested_target": "The specific person, office, or body to address this petition to",
"suggested_tags": ["array", "of", "4-6", "relevant", "tags", "including-both-categories"],
"suggested_goal": 25000,
"alliance_insight": "2-3 sentences explaining why this alliance between ${marketA.category} and ${marketB.category} is politically powerful and surprising.",
"group_a_appeal": "2-3 sentences explaining specifically why ${marketA.category} supporters would sign this petition.",
"group_b_appeal": "2-3 sentences explaining specifically why ${marketB.category} supporters would sign this petition.",
"impact_multiplier": "2-3 sentences explaining why this joint petition achieves more than two separate petitions. Be specific about the political dynamics."
}`;
// 2026-05-05 (tick 29): migrated to lib/gemini.ts wrapper.
const r = await callGemini<Record<string, unknown>>({ prompt, maxTokens: 4096, temperature: 0.85, timeoutMs: 30_000 });
if (!r.ok) {
console.error('[orbit/alliance] gemini failed:', r.reason, r.detail);
return NextResponse.json(
{ error: r.reason === 'no_key' ? 'AI service not configured' : 'AI generation failed' },
{ status: r.status },
);
}
const parsed = r.data as Record<string, unknown>;
// Create petition
const title = String(parsed.title || `Alliance: ${marketA.category} + ${marketB.category}`);
let slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
const existingSlug = await query(`SELECT id FROM petitions WHERE slug = $1`, [slug]);
if (existingSlug.rows.length > 0) {
slug = `${slug}-${Date.now().toString(36)}`;
}
const userResult = await query(`SELECT id FROM users LIMIT 1`);
const userId = userResult.rows[0]?.id;
if (!userId) {
return NextResponse.json({ error: 'No user found in system' }, { status: 500 });
}
const petitionResult = await query(
`INSERT INTO petitions (user_id, title, slug, summary, body_html, body_text, target, category, tags,
signature_goal, ai_source, ai_topic_data, status)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
RETURNING *`,
[
userId,
title,
slug,
parsed.summary || null,
parsed.body_html || '<p>Alliance petition content</p>',
parsed.body_text || null,
parsed.suggested_target || null,
'alliance',
parsed.suggested_tags || null,
parsed.suggested_goal || 25000,
'kalshi_alliance',
JSON.stringify({
market_a: { ticker: marketA.ticker, title: marketA.title, category: marketA.category },
market_b: { ticker: marketB.ticker, title: marketB.title, category: marketB.category },
alliance_insight: parsed.alliance_insight,
group_a_appeal: parsed.group_a_appeal,
group_b_appeal: parsed.group_b_appeal,
impact_multiplier: parsed.impact_multiplier,
generated_at: new Date().toISOString(),
}),
'draft',
],
);
const petition = petitionResult.rows[0];
// Create TWO orbit_links — one per market
const [linkA, linkB] = await Promise.all([
query(
`INSERT INTO orbit_links (market_id, petition_id, link_type, strength, ai_reasoning)
VALUES ($1, $2, 'alliance', 0.9, $3)
ON CONFLICT (market_id, petition_id) DO UPDATE SET
link_type = 'alliance', ai_reasoning = EXCLUDED.ai_reasoning, strength = 0.9
RETURNING *`,
[marketA.id, petition.id, parsed.group_a_appeal || 'Alliance petition member.']
),
query(
`INSERT INTO orbit_links (market_id, petition_id, link_type, strength, ai_reasoning)
VALUES ($1, $2, 'alliance', 0.9, $3)
ON CONFLICT (market_id, petition_id) DO UPDATE SET
link_type = 'alliance', ai_reasoning = EXCLUDED.ai_reasoning, strength = 0.9
RETURNING *`,
[marketB.id, petition.id, parsed.group_b_appeal || 'Alliance petition member.']
),
]);
// Update both markets' petition_relevance
await Promise.all([
query(
`UPDATE orbit_markets SET petition_relevance = GREATEST(petition_relevance, 85), suggested_petition = $2 WHERE id = $1`,
[marketA.id, title]
),
query(
`UPDATE orbit_markets SET petition_relevance = GREATEST(petition_relevance, 85), suggested_petition = $2 WHERE id = $1`,
[marketB.id, title]
),
]);
await auditLog('orbit.alliance_generated', 'petition', petition.id, {
market_a: marketA.ticker,
market_b: marketB.ticker,
category_a: marketA.category,
category_b: marketB.category,
tone: toneDesc,
});
return NextResponse.json({
petition: {
...petition,
alliance_insight: parsed.alliance_insight || '',
group_a_appeal: parsed.group_a_appeal || '',
group_b_appeal: parsed.group_b_appeal || '',
impact_multiplier: parsed.impact_multiplier || '',
},
links: [linkA.rows[0], linkB.rows[0]],
markets: {
a: { ticker: marketA.ticker, title: marketA.title, category: marketA.category },
b: { ticker: marketB.ticker, title: marketB.title, category: marketB.category },
},
});
} catch (err) {
console.error('[api/orbit/suggest-alliance] Error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to generate alliance petition' }, { status: 500 });
}
}