File size: 5,483 Bytes
de40b1a
 
 
f667d47
de40b1a
 
f667d47
de40b1a
f667d47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de40b1a
 
 
 
 
 
f667d47
 
 
de40b1a
 
 
 
f667d47
 
 
de40b1a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6b6c96
 
 
 
 
de40b1a
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
134
135
136
137
138
139
140
141
/**
 * FlowState AI Agent Engine β€” Autonomous churn detection & retention
 * 5-signal scoring: inactivity, volume decline, protocol diversity, streak, liquidation
 * Supports configurable thresholds and pre-churn early warning detection.
 */
import { ChurnRisk } from './types'
import { sendCustomEvent } from './torque-mcp'

export interface ScoringThresholds {
  inactivityCritical: number   // days inactive β†’ critical signal (default 30)
  inactivityHigh: number       // days inactive β†’ high signal (default 14)
  inactivityMedium: number     // days inactive β†’ medium signal (default 7)
  volumeDropCritical: number   // % volume drop β†’ critical signal (default 80)
  volumeDropHigh: number       // % volume drop β†’ high signal (default 50)
  volumeDropMedium: number     // % volume drop β†’ medium signal (default 25)
  preChurnDaysTrader: number   // days quiet for trader type (default 3)
  preChurnDaysLP: number       // days quiet for LP type (default 5)
  preChurnDaysStaker: number   // days quiet for staker type (default 7)
}

export const DEFAULT_THRESHOLDS: ScoringThresholds = {
  inactivityCritical: 30,
  inactivityHigh: 14,
  inactivityMedium: 7,
  volumeDropCritical: 80,
  volumeDropHigh: 50,
  volumeDropMedium: 25,
  preChurnDaysTrader: 3,
  preChurnDaysLP: 5,
  preChurnDaysStaker: 7,
}

export type WalletType = 'trader' | 'lp' | 'staker'

const TRADER_PROTOCOLS = new Set(['Jupiter', 'Raydium', 'Drift', 'Tensor'])
const LP_PROTOCOLS = new Set(['Kamino', 'Marginfi', 'Meteora'])

export function classifyWalletType(protocols: string[]): WalletType {
  const traderCount = protocols.filter(p => TRADER_PROTOCOLS.has(p)).length
  const lpCount = protocols.filter(p => LP_PROTOCOLS.has(p)).length
  if (traderCount >= 2) return 'trader'
  if (lpCount >= 1) return 'lp'
  return 'staker'
}

export function detectPreChurn(
  daysInactive: number,
  streak: number,
  protocols: string[],
  thresholds: ScoringThresholds = DEFAULT_THRESHOLDS
): { isPreChurn: boolean; walletType: WalletType; threshold: number } {
  const walletType = classifyWalletType(protocols)
  const threshold =
    walletType === 'trader'
      ? thresholds.preChurnDaysTrader
      : walletType === 'lp'
        ? thresholds.preChurnDaysLP
        : thresholds.preChurnDaysStaker
  // Pre-churn: below inactivity medium threshold but streak recently broken
  const isPreChurn =
    daysInactive >= threshold &&
    daysInactive < thresholds.inactivityMedium &&
    streak <= 2
  return { isPreChurn, walletType, threshold }
}

export function calculateChurnScore(
  activity: {
    daysInactive: number; volumeDropPct: number; uniqueProtocols: number;
    currentStreak: number; hasLiquidation: boolean
  },
  thresholds: ScoringThresholds = DEFAULT_THRESHOLDS
): { score: number; risk: ChurnRisk; signals: string[] } {
  let score = 0
  const signals: string[] = []

  // Signal 1: Inactivity (0-30pts)
  const inactScore = Math.min(activity.daysInactive * 3, 30)
  score += inactScore
  if (activity.daysInactive >= thresholds.inactivityMedium) {
    signals.push('Inactive ' + activity.daysInactive + ' days')
  }

  // Signal 2: Volume decline (0-25pts)
  if (activity.volumeDropPct > 0) {
    score += Math.min(activity.volumeDropPct / 4, 25)
    if (activity.volumeDropPct >= thresholds.volumeDropMedium) {
      signals.push('Volume dropped ' + activity.volumeDropPct.toFixed(0) + '%')
    }
  }

  // Signal 3: Protocol diversity (0-15pts)
  if (activity.uniqueProtocols <= 1) { score += 15; signals.push('Single protocol β€” low engagement') }
  else if (activity.uniqueProtocols <= 2) { score += 8; signals.push('Limited protocol diversity') }

  // Signal 4: Streak broken (0-15pts)
  if (activity.currentStreak === 0) { score += 15; signals.push('Activity streak broken') }
  else if (activity.currentStreak < 3) score += 5

  // Signal 5: Liquidation (0-15pts)
  if (activity.hasLiquidation) { score += 15; signals.push('Recent liquidation event') }

  score = Math.min(Math.max(score, 0), 100)

  let risk: ChurnRisk
  if (score >= 80) risk = 'critical'
  else if (score >= 60) risk = 'high'
  else if (score >= 40) risk = 'medium'
  else if (score >= 20) risk = 'low'
  else risk = 'safe'

  return { score, risk, signals }
}

export function selectResponse(risk: ChurnRisk, wallet: string): { action: string; description: string }[] {
  const responses: { action: string; description: string }[] = []
  responses.push({ action: 'detect', description: 'Detected ' + risk + ' churn risk for ' + wallet.slice(0, 8) + '...' })

  switch (risk) {
    case 'critical':
      responses.push({ action: 'gift', description: 'Sending 0.5 SOL gift via Anti-Churn campaign' })
      responses.push({ action: 'raffle', description: 'Enrolling in Comeback Raffle with 5x multiplier' })
      break
    case 'high':
      responses.push({ action: 'raffle', description: 'Enrolling in Comeback Raffle with 3x multiplier' })
      break
    case 'medium':
      responses.push({ action: 'rebate', description: 'Activating 2x rebate boost for 48 hours' })
      break
  }
  return responses
}

export async function executeResponse(wallet: string, action: string) {
  const eventName =
    action === 'gift' || action === 'raffle' ? 'churn_risk_high'
    : action === 'rebate' ? 'churn_risk_medium'
    : 'inactivity_detected'
  return sendCustomEvent(wallet, eventName, { triggeredAction: action, detectedBy: 'flowstate-ai-agent', timestamp: new Date().toISOString() })
}