File size: 4,580 Bytes
7200823
c6b6c96
f667d47
 
c6b6c96
f667d47
c6b6c96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f667d47
c6b6c96
 
 
 
 
 
f667d47
 
 
 
 
c6b6c96
 
 
 
 
 
 
 
f667d47
c6b6c96
 
 
 
 
 
 
 
 
 
 
 
 
 
f667d47
c6b6c96
 
 
 
 
 
 
f667d47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6b6c96
 
 
 
f667d47
 
 
 
 
 
 
 
 
 
 
c6b6c96
 
 
f667d47
c6b6c96
 
 
 
 
 
 
 
 
f667d47
 
 
 
 
c6b6c96
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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',
    ],
  })
}