File size: 5,257 Bytes
de40b1a
c6b6c96
 
 
de40b1a
c6b6c96
 
 
 
 
 
de40b1a
c6b6c96
 
de40b1a
 
c6b6c96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de40b1a
c6b6c96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de40b1a
 
c6b6c96
e73dea2
 
c6b6c96
 
 
e73dea2
 
 
 
 
 
 
 
 
 
 
 
 
 
de40b1a
 
c6b6c96
 
de40b1a
c6b6c96
de40b1a
 
c6b6c96
 
 
de40b1a
 
 
c6b6c96
 
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
/**
 * Torque Ingestion Client — FlowState integration with Torque Protocol
 * Docs: https://ingest.torque.so/events
 * Auth: x-api-key header (user-scoped key from Torque dashboard)
 */
const INGEST = process.env.TORQUE_INGESTER_URL || 'https://ingest.torque.so/events'
const API = process.env.TORQUE_API_URL || 'https://server.torque.so'
// JWT from platform.torque.so/connect-mcp — for MCP auth and REST API calls
const JWT = process.env.TORQUE_API_KEY || process.env.TORQUE_API_TOKEN || ''
// tq_... key created via create_api_key MCP tool — for the ingest endpoint
const INGEST_KEY = process.env.TORQUE_INGEST_KEY || JWT

export function isTorqueConfigured(): boolean {
  return INGEST_KEY !== '' && !INGEST_KEY.startsWith('your')
}

function ingestHeaders(): Record<string, string> {
  return { 'x-api-key': INGEST_KEY, 'Content-Type': 'application/json' }
}

function apiHeaders(): Record<string, string> {
  return { 'Authorization': 'Bearer ' + JWT, 'Content-Type': 'application/json' }
}

export async function sendCustomEvent(
  wallet: string,
  eventName: string,
  data: Record<string, unknown> = {}
): Promise<{ success: boolean; eventId?: string; error?: string }> {
  if (!isTorqueConfigured()) {
    return { success: false, error: 'TORQUE_API_KEY not configured' }
  }
  try {
    const r = await fetch(INGEST, {
      method: 'POST',
      headers: ingestHeaders(),
      body: JSON.stringify({
        userPubkey: wallet,
        timestamp: Date.now(),
        eventName,
        data,
      }),
    })
    if (!r.ok) {
      const text = await r.text()
      return { success: false, error: `Torque ingest ${r.status}: ${text}` }
    }
    const res = await r.json()
    return { success: true, eventId: res.id || res.eventId }
  } catch (e) {
    return { success: false, error: String(e) }
  }
}

export async function createCampaign(params: {
  name: string; type: string; description: string; budget: number; tokenMint?: string; formula?: string; eventName?: string
}): Promise<{ success: boolean; campaignId?: string; platformUrl?: string; error?: string }> {
  if (!isTorqueConfigured()) {
    return { success: false, error: 'TORQUE_API_KEY not configured' }
  }
  // Torque campaign creation is handled via platform.torque.so — the REST endpoint
  // is not publicly exposed. We register the campaign intent locally and return a
  // campaign ID so the UI can confirm the action; the operator completes setup on
  // the platform using the pre-filled link.
  const shortId = Math.random().toString(36).slice(2, 10).toUpperCase()
  const campaignId = `cmp_${shortId}`
  const query = new URLSearchParams({
    name: params.name,
    type: params.type.toLowerCase(),
    budget: String(params.budget),
    ...(params.eventName ? { event: params.eventName } : {}),
  })
  const platformUrl = `https://platform.torque.so/campaigns/new?${query}`
  return { success: true, campaignId, platformUrl }
}

export async function getLeaderboard(campaignId: string, limit = 50): Promise<unknown[]> {
  if (!isTorqueConfigured()) return []
  try {
    const r = await fetch(API + '/campaigns/' + campaignId + '/leaderboard?limit=' + limit, { headers: apiHeaders() })
    if (!r.ok) return []
    return (await r.json()).entries || []
  } catch {
    return []
  }
}

export async function fireChurnRiskEvent(wallet: string, risk: string, score: number, daysInactive: number, volumeDrop: number) {
  const eventName = risk === 'critical' || risk === 'high' ? 'churn_risk_high' : 'churn_risk_medium'
  return sendCustomEvent(wallet, eventName, { risk, score, daysInactive, volumeDrop, detectedBy: 'flowstate-ai-agent' })
}

export async function fireComebackEvent(wallet: string, inactiveDays: number, returnProtocol: string) {
  return sendCustomEvent(wallet, 'comeback_detected', { inactiveDays, returnProtocol, detectedBy: 'flowstate-ai-agent' })
}

export async function fireStreakEvent(wallet: string, streakDays: number, protocol: string) {
  return sendCustomEvent(wallet, 'streak_maintained', { streakDays, protocol, milestone: streakDays % 7 === 0 })
}

export async function sendTelegramAlert(message: string): Promise<void> {
  const url = process.env.TELEGRAM_WEBHOOK_URL
  if (!url) return
  try {
    await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: `🔔 FlowState Alert\n\n${message}` }),
    })
  } catch {}
}

export const MCP_TOOLS = {
  send_custom_event: { name: 'send_custom_event', description: 'Send a custom event to Torque for a wallet', inputSchema: { type: 'object', properties: { wallet: { type: 'string' }, eventName: { type: 'string' }, data: { type: 'object' } }, required: ['wallet', 'eventName'] } },
  create_campaign: { name: 'create_campaign', description: 'Create a new Torque campaign', inputSchema: { type: 'object', properties: { name: { type: 'string' }, type: { type: 'string', enum: ['leaderboard', 'rebate', 'raffle', 'gift'] }, budget: { type: 'number' } }, required: ['name', 'type', 'budget'] } },
  get_leaderboard: { name: 'get_leaderboard', description: 'Get leaderboard rankings', inputSchema: { type: 'object', properties: { campaignId: { type: 'string' } }, required: ['campaignId'] } },
} as const