/** * Opportunity Scanner * * Runs on a schedule (every 30 min). The agent: * 1. Checks its P&L and available revenue * 2. Scans for yield/staking/swap opportunities via MoonPay + Zerion * 3. Creates delegation requests for each opportunity * 4. Owner wakes up to a queue of proposals to approve/reject * * The agent NEVER acts without approval. It only proposes. */ import { createDelegation, getPnL, getTreasuryAddress, generateHeuristicInterpretation } from './governance.js'; import { logAuditEvent } from './audit.js'; import { notifyDelegationRequest } from './xmtp.js'; interface Opportunity { reason: string; chains: string[]; operations: string[]; maxAmountPerAction: string; totalBudget: string; allowedRecipients: string[]; expiresInMinutes: number; } // ── Known yield opportunities on Base ────────────────── const BASE_OPPORTUNITIES: Opportunity[] = [ { reason: 'Stake USDC on Aave V3 (Base) for ~3.5% APY', chains: ['eip155:8453'], operations: ['sign_tx'], maxAmountPerAction: '5.00', totalBudget: '5.00', allowedRecipients: ['0x18cd499e3d7ed42FEBa981ac9236A278E4Cdc2ee'], // Aave Pool on Base expiresInMinutes: 60, }, { reason: 'Swap 50% of USDC earnings to ETH via Uniswap (portfolio diversification)', chains: ['eip155:8453'], operations: ['sign_tx'], maxAmountPerAction: '2.50', totalBudget: '2.50', allowedRecipients: ['0x2626664c2603336E57B271c5C0b26F421741e481'], // Uniswap Universal Router on Base expiresInMinutes: 30, }, { reason: 'Buy VVV tokens for Venice DIEM staking (perpetual compute access)', chains: ['eip155:8453'], operations: ['sign_tx'], maxAmountPerAction: '7.50', totalBudget: '7.50', allowedRecipients: [ '0x2626664c2603336E57B271c5C0b26F421741e481', // Uniswap (buy VVV) '0x321b7ff75154472b18edb199033ff4d116f340ff', // Venice staking contract ], expiresInMinutes: 120, }, { reason: 'Top up Venice inference credits with earned USDC (self-sustaining compute)', chains: ['eip155:8453'], operations: ['sign_tx'], maxAmountPerAction: '5.00', totalBudget: '5.00', allowedRecipients: ['0x2670B922ef37C7Df47158725C0CC407b5382293F'], // Venice receiver expiresInMinutes: 60, }, ]; // ── Scanner ──────────────────────────────────────────── export async function scanOpportunities(): Promise { const pnl = getPnL(); const address = getTreasuryAddress(); if (pnl.profit <= 0) { console.log('[Scanner] No profit yet — skipping opportunity scan'); return 0; } console.log(`[Scanner] Profit: $${pnl.profit.toFixed(4)} — scanning for opportunities...`); let created = 0; for (const opp of BASE_OPPORTUNITIES) { // Only propose if we can afford it const budget = parseFloat(opp.totalBudget); // Always propose opportunities for the demo — in production, threshold by revenue // if (budget > pnl.profit * 100) continue; const expiresAt = new Date(Date.now() + opp.expiresInMinutes * 60000).toISOString(); const delegation = createDelegation({ agentAddress: address, reason: opp.reason, chains: opp.chains, operations: opp.operations, maxAmountPerAction: opp.maxAmountPerAction, totalBudget: opp.totalBudget, allowedRecipients: opp.allowedRecipients, expiresAt, }); delegation.aiInterpretation = generateHeuristicInterpretation(delegation); await logAuditEvent('requested', delegation.id, delegation.reason, 0); await notifyDelegationRequest(delegation); console.log(`[Scanner] Proposed: ${opp.reason} ($${opp.totalBudget})`); created++; } console.log(`[Scanner] Created ${created} delegation proposals`); return created; } // ── Schedule ─────────────────────────────────────────── let scanInterval: ReturnType | null = null; export function startScanner(intervalMinutes: number = 30): void { console.log(`[Scanner] Starting — scanning every ${intervalMinutes} minutes`); // Run once immediately after a short delay (let server init first) setTimeout(() => scanOpportunities().catch(console.error), 10000); scanInterval = setInterval( () => scanOpportunities().catch(console.error), intervalMinutes * 60000, ); } export function stopScanner(): void { if (scanInterval) { clearInterval(scanInterval); scanInterval = null; } }