import { Client, IdentifierKind } from '@xmtp/node-sdk' import type { Signer, Identifier, Dm } from '@xmtp/node-sdk' import { privateKeyToAccount } from 'viem/accounts' import { hexToBytes } from 'viem' import { getConfig } from './config' const HUMAN_ADDRESS = process.env.HUMAN_XMTP_ADDRESS || '' // Module-level state let xmtpClient: Client | null = null let humanDm: Dm | null = null let lastDeniedAgent = '' function humanIdentifier(): Identifier { return { identifier: HUMAN_ADDRESS.toLowerCase(), identifierKind: IdentifierKind.Ethereum, } } // ---------------------------------------------------------------- // Incoming message listener — detects "override" replies // ---------------------------------------------------------------- async function startMessageListener(): Promise { if (!xmtpClient) return try { const stream = await xmtpClient.conversations.streamAllMessages() for await (const message of stream) { try { const text = typeof message.content === 'string' ? message.content.trim().toLowerCase() : '' if (text.startsWith('override') && lastDeniedAgent) { const port = process.env.PORT || '4021' const agentToOverride = lastDeniedAgent lastDeniedAgent = '' const res = await fetch(`http://localhost:${port}/api/override/${agentToOverride}`, { method: 'POST' }) if (res.ok) { console.log(`[XMTP] Override accepted for agent: ${agentToOverride}`) } } } catch (err) { console.error('[XMTP] Error processing message:', err) } } } catch (err) { console.error('[XMTP] Message stream error:', err) } } // ---------------------------------------------------------------- // Send helper — finds or creates DM with human, caches it // ---------------------------------------------------------------- async function sendToHuman(text: string): Promise { if (!xmtpClient) return try { if (!humanDm) { humanDm = await xmtpClient.conversations.createDmWithIdentifier(humanIdentifier()) } await humanDm.sendText(text) } catch (err) { console.error('[XMTP] Send error:', err) humanDm = null // reset so next call retries } } // ---------------------------------------------------------------- // Exported functions // ---------------------------------------------------------------- export async function initXMTP(): Promise { if (!getConfig().xmtp?.enabled) { console.log('[XMTP] Disabled by config') return } const privateKey = process.env.XMTP_PRIVATE_KEY if (!privateKey || !HUMAN_ADDRESS) { console.log('[XMTP] Not configured — notifications disabled') return } try { const account = privateKeyToAccount(privateKey as `0x${string}`) const signer: Signer = { type: 'EOA', getIdentifier: (): Identifier => ({ identifier: account.address.toLowerCase(), identifierKind: IdentifierKind.Ethereum, }), signMessage: async (message: string): Promise => { const sig = await account.signMessage({ message }) return hexToBytes(sig) }, } // `as any` required: ClientOptions is a union type that doesn't survive // Omit<> in Client.create's signature — SDK types collapse the union. xmtpClient = await Client.create(signer, { env: 'production' } as any) // Warm up the DM channel humanDm = await xmtpClient.conversations.createDmWithIdentifier(humanIdentifier()) // Start listening for override replies (fire and forget) startMessageListener().catch((err) => { console.error('[XMTP] Listener startup error:', err) }) console.log('[XMTP] Initialized — notifications enabled') } catch (err) { console.error('[XMTP] Init failed:', err) } } export async function notifyDenial( agent: string, amount: number, trustScore: number, tier: string, dailyLimit: number, dailySpent: number, reason: string, txTo: string, ): Promise { if (!xmtpClient) return lastDeniedAgent = agent const msg = [ `⚠️ Transaction denied`, `Agent: ${agent}`, `Amount: $${amount.toFixed(2)} USDC → ${txTo}`, `Trust score: ${trustScore} (${tier} tier)`, `Daily limit: $${dailyLimit} | Spent today: $${dailySpent.toFixed(2)}`, `Reason: ${reason}`, ``, `Reply "override" to approve this transaction.`, ].join('\n') await sendToHuman(msg) } export async function notifyTrustChange( agent: string, oldScore: number, newScore: number, oldTier: string, newTier: string, reason: string, ): Promise { if (!xmtpClient) return const msg = [ `📊 Trust update: ${agent}`, `Score: ${oldScore} → ${newScore} | Tier: ${oldTier} → ${newTier}`, `Reason: ${reason}`, ].join('\n') await sendToHuman(msg) } export async function notifyBudgetWarning( agent: string, spent: number, limit: number, ): Promise { if (!xmtpClient) return const percentage = Math.round((spent / limit) * 100) const remaining = limit - spent const msg = [ `💰 Budget alert: ${agent}`, `Daily spend: $${spent.toFixed(2)} / $${limit} (${percentage}%)`, `Remaining: $${remaining.toFixed(2)}`, ].join('\n') await sendToHuman(msg) } export async function notifyAnomaly( agent: string, pattern: string, action: string, riskPenalty: number, oldScore: number, newScore: number, ): Promise { if (!xmtpClient) return const msg = [ `🚨 Anomaly: ${agent}`, `Pattern: ${pattern}`, `Action: ${action}`, `Risk penalty: -${riskPenalty}`, `Trust score: ${oldScore} → ${newScore}`, ].join('\n') await sendToHuman(msg) }