flowstate / src /lib /torque-mcp.ts
muthuk1's picture
feat: recovery attribution, pre-churn warnings, recovery card, threshold editor, telegram alerts
f667d47
/**
* 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