flowstate / src /app /api /agent /scan /route.ts
muthuk1's picture
feat: recovery attribution, pre-churn warnings, recovery card, threshold editor, telegram alerts
f667d47
import { NextResponse } from 'next/server'
import { wallets } from '@/lib/mock-data'
import { calculateChurnScore, detectPreChurn } from '@/lib/agent-engine'
import { sendCustomEvent, isTorqueConfigured, sendTelegramAlert } from '@/lib/torque-mcp'
import { pushEvent } from '@/lib/event-store'
import { recordIntervention, recordRecovery } from '@/lib/attribution-store'
function walletToSignals(w: typeof wallets[0]) {
const daysMatch = w.lastActive.match(/(\d+)d/)
const daysInactive = daysMatch ? parseInt(daysMatch[1]) : 0
return {
daysInactive,
volumeDropPct: w.streak === 0 ? 80 : Math.max(0, (10 - Math.min(w.streak, 10)) * 8),
uniqueProtocols: w.protocols.length,
currentStreak: w.streak,
hasLiquidation: false,
}
}
export async function POST() {
const configured = isTorqueConfigured()
const detections: Array<{
wallet: string; risk: string; score: number; eventName: string;
eventSent: boolean; eventId?: string; error?: string; preChurn?: boolean; walletType?: string
}> = []
for (const wallet of wallets) {
const signals = walletToSignals(wallet)
const { score, risk } = calculateChurnScore(signals)
// Pre-churn early warning: fire inactivity_detected before full churn threshold
const { isPreChurn, walletType } = detectPreChurn(
signals.daysInactive, signals.currentStreak, wallet.protocols
)
if (risk === 'critical' || risk === 'high' || risk === 'medium') {
const eventName = risk === 'critical' || risk === 'high' ? 'churn_risk_high' : 'churn_risk_medium'
const result = configured
? await sendCustomEvent(wallet.address, eventName, {
risk, score,
daysInactive: signals.daysInactive,
protocols: wallet.protocols,
walletType,
detectedBy: 'flowstate-ai-agent',
})
: { success: false, error: 'TORQUE_INGEST_KEY not configured' }
if (result.success && result.eventId) {
pushEvent({
ingestionId: result.eventId,
wallet: wallet.address,
eventName,
risk,
score,
firedAt: new Date().toISOString(),
source: 'scan',
})
recordIntervention(wallet.address, eventName, score)
}
detections.push({
wallet: wallet.address, risk, score, eventName,
eventSent: result.success,
eventId: result.eventId,
error: result.error,
walletType,
})
} else if (isPreChurn) {
// Early warning: soft nudge before reaching churn threshold
const result = configured
? await sendCustomEvent(wallet.address, 'inactivity_detected', {
risk: 'pre_churn',
score,
daysInactive: signals.daysInactive,
walletType,
preChurnWarning: true,
detectedBy: 'flowstate-ai-agent',
})
: { success: false, error: 'TORQUE_INGEST_KEY not configured' }
if (result.success && result.eventId) {
pushEvent({
ingestionId: result.eventId,
wallet: wallet.address,
eventName: 'inactivity_detected',
risk: 'low',
score,
firedAt: new Date().toISOString(),
source: 'scan',
})
}
detections.push({
wallet: wallet.address, risk: 'pre_churn', score, eventName: 'inactivity_detected',
eventSent: result.success, eventId: result.eventId, error: result.error,
preChurn: true, walletType,
})
}
}
// Telegram alert when ≥5 critical wallets detected in one scan
const criticalCount = detections.filter(d => d.risk === 'critical').length
if (criticalCount >= 5) {
const fired = detections.filter(d => d.eventSent).length
await sendTelegramAlert(
`🚨 ${criticalCount} critical wallets detected in scan\n` +
`📡 ${fired}/${detections.length} Torque events confirmed\n` +
`⏱ ${new Date().toLocaleTimeString('en-US', { hour12: false })} UTC`
)
}
return NextResponse.json({
detections,
count: detections.length,
preChurnCount: detections.filter(d => d.preChurn).length,
configured,
timestamp: new Date().toISOString(),
})
}
export async function GET() {
return NextResponse.json({
status: 'active',
configured: isTorqueConfigured(),
capabilities: [
'churn_detection', 'pre_churn_early_warning', 'wallet_type_classification',
'auto_campaign_creation', 'comeback_detection', 'streak_tracking',
'recovery_attribution', 'telegram_alerts',
],
})
}